├── .watchmanconfig ├── .gitattributes ├── app.json ├── src ├── images │ ├── logo.jpg │ └── index.js ├── store │ └── index.js ├── utils │ ├── LanguageUtil.js │ ├── Color.js │ ├── httpUtil.js │ ├── AuthUtil.js │ ├── screenUtil.js │ ├── language │ │ ├── zh-Hant.js │ │ ├── zh-Hans.js │ │ └── en.js │ ├── Utility.js │ ├── storageUtil.js │ └── Toast.js ├── reducers │ ├── system.js │ ├── project.js │ ├── index.js │ ├── guide.js │ ├── wxArticle.js │ ├── coin.js │ ├── user.js │ ├── collect.js │ ├── search.js │ └── home.js ├── styles │ └── globalStyles.js ├── component │ ├── ProgressBar.js │ ├── ListFooter.js │ ├── LoadingView.js │ ├── ArticleTabComponent.js │ ├── BottomTabBar.js │ ├── CommonFlatList.js │ ├── Banner.js │ ├── Touchable.js │ ├── NavBar.js │ ├── ArticleItemRow.js │ └── ArticleFlatList.js ├── screen │ ├── article │ │ ├── WebViewScreen.js │ │ ├── ArticleTabScreen.js │ │ ├── SearchArticleScreen.js │ │ └── SearchScreen.js │ ├── tabs │ │ ├── ProjectScreen.js │ │ ├── WxArticleScreen.js │ │ ├── HomeScreen.js │ │ └── SystemScreen.js │ └── drawer │ │ ├── LanguageScreen.js │ │ ├── WebsitesScreen.js │ │ ├── CollectScreen.js │ │ ├── AboutScreen.js │ │ ├── CoinDetailScreen.js │ │ └── LoginScreen.js ├── actions │ ├── actionType.js │ └── action-creator.js ├── api │ └── index.js ├── service │ └── setAxios.js └── index.js ├── .eslintrc.js ├── screenshot ├── iOS_01.png ├── iOS_02.png ├── iOS_03.png ├── iOS_04.png ├── iOS_05.png ├── iOS_06.png ├── iOS_07.png ├── iOS_08.png ├── iOS_09.png ├── iOS_10.png ├── iOS_11.png ├── iOS_12.png ├── android_01.png ├── android_02.png ├── android_03.png ├── android_04.png ├── android_05.png ├── android_06.png ├── android_07.png ├── android_08.png ├── android_09.png ├── android_10.png ├── android_11.png └── android_12.png ├── babel.config.js ├── android ├── app │ ├── debug.keystore │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── splash_bg.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── layout │ │ │ │ │ └── launch_screen.xml │ │ │ ├── assets │ │ │ │ └── fonts │ │ │ │ │ ├── Entypo.ttf │ │ │ │ │ ├── Feather.ttf │ │ │ │ │ ├── Zocial.ttf │ │ │ │ │ ├── AntDesign.ttf │ │ │ │ │ ├── EvilIcons.ttf │ │ │ │ │ ├── Fontisto.ttf │ │ │ │ │ ├── Foundation.ttf │ │ │ │ │ ├── Ionicons.ttf │ │ │ │ │ ├── Octicons.ttf │ │ │ │ │ ├── FontAwesome.ttf │ │ │ │ │ ├── MaterialIcons.ttf │ │ │ │ │ ├── SimpleLineIcons.ttf │ │ │ │ │ ├── FontAwesome5_Brands.ttf │ │ │ │ │ ├── FontAwesome5_Regular.ttf │ │ │ │ │ ├── FontAwesome5_Solid.ttf │ │ │ │ │ └── MaterialCommunityIcons.ttf │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── rn_wanandroid │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ └── debug │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ ├── build_defs.bzl │ └── BUCK ├── keystores │ └── app-key.jks ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── settings.gradle ├── build.gradle ├── gradle.properties └── gradlew.bat ├── ios ├── RN_WanAndroid │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── LaunchImage.launchimage │ │ │ ├── splash_bg.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── main.m │ ├── AppDelegate.m │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.xib ├── RN_WanAndroid.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── RN_WanAndroidTests │ ├── Info.plist │ └── RN_WanAndroidTests.m ├── RN_WanAndroid-tvOSTests │ └── Info.plist ├── RN_WanAndroid-tvOS │ └── Info.plist ├── Podfile └── RN_WanAndroid.xcodeproj │ └── xcshareddata │ └── xcschemes │ ├── RN_WanAndroid.xcscheme │ └── RN_WanAndroid-tvOS.xcscheme ├── .buckconfig ├── .prettierrc.js ├── __tests__ └── App-test.js ├── metro.config.js ├── index.js ├── .gitignore ├── package.json ├── App.js ├── .flowconfig └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RN_WanAndroid", 3 | "displayName": "RN_WanAndroid" 4 | } -------------------------------------------------------------------------------- /src/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/src/images/logo.jpg -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | }; 5 | -------------------------------------------------------------------------------- /screenshot/iOS_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_01.png -------------------------------------------------------------------------------- /screenshot/iOS_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_02.png -------------------------------------------------------------------------------- /screenshot/iOS_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_03.png -------------------------------------------------------------------------------- /screenshot/iOS_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_04.png -------------------------------------------------------------------------------- /screenshot/iOS_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_05.png -------------------------------------------------------------------------------- /screenshot/iOS_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_06.png -------------------------------------------------------------------------------- /screenshot/iOS_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_07.png -------------------------------------------------------------------------------- /screenshot/iOS_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_08.png -------------------------------------------------------------------------------- /screenshot/iOS_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_09.png -------------------------------------------------------------------------------- /screenshot/iOS_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_10.png -------------------------------------------------------------------------------- /screenshot/iOS_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_11.png -------------------------------------------------------------------------------- /screenshot/iOS_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/iOS_12.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /screenshot/android_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_01.png -------------------------------------------------------------------------------- /screenshot/android_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_02.png -------------------------------------------------------------------------------- /screenshot/android_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_03.png -------------------------------------------------------------------------------- /screenshot/android_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_04.png -------------------------------------------------------------------------------- /screenshot/android_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_05.png -------------------------------------------------------------------------------- /screenshot/android_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_06.png -------------------------------------------------------------------------------- /screenshot/android_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_07.png -------------------------------------------------------------------------------- /screenshot/android_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_08.png -------------------------------------------------------------------------------- /screenshot/android_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_09.png -------------------------------------------------------------------------------- /screenshot/android_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_10.png -------------------------------------------------------------------------------- /screenshot/android_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_11.png -------------------------------------------------------------------------------- /screenshot/android_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/screenshot/android_12.png -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/debug.keystore -------------------------------------------------------------------------------- /android/keystores/app-key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/keystores/app-key.jks -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RN玩安卓 3 | 4 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Entypo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Entypo.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Feather.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Feather.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Zocial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Zocial.ttf -------------------------------------------------------------------------------- /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/AntDesign.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/AntDesign.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/EvilIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/EvilIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Fontisto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Fontisto.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Foundation.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Foundation.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Ionicons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/Octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/Octicons.ttf -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | }; 7 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/FontAwesome.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/MaterialIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/SimpleLineIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/SimpleLineIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/splash_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xxhdpi/splash_bg.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Brands.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Regular.ttf -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/FontAwesome5_Solid.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/images/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-20 3 | */ 4 | const images = { 5 | logoIcon: require('./logo.jpg'), 6 | }; 7 | 8 | export default images; 9 | -------------------------------------------------------------------------------- /android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/assets/fonts/MaterialCommunityIcons.ttf -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Images.xcassets/LaunchImage.launchimage/splash_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aijason/RN_WanAndroid/HEAD/ios/RN_WanAndroid/Images.xcassets/LaunchImage.launchimage/splash_bg.png -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'RN_WanAndroid' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import {createStore, applyMiddleware} from 'redux'; 5 | import thunk from 'redux-thunk'; 6 | import rootReducer from '../reducers'; 7 | 8 | const index = createStore(rootReducer, applyMiddleware(thunk)); 9 | export default index; 10 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /__tests__/App-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: false, 13 | inlineRequires: false, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/utils/LanguageUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-16 3 | */ 4 | import I18n from 'react-native-i18n'; 5 | import en from './language/en'; 6 | import zhHans from './language/zh-Hans'; 7 | import zhHant from './language/zh-Hant'; 8 | 9 | I18n.defaultLocale = 'zhHans'; 10 | 11 | I18n.fallbacks = true; 12 | 13 | I18n.translations = { 14 | en, 15 | zhHans, 16 | zhHant, 17 | }; 18 | 19 | export default I18n; 20 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | console.disableYellowBox = true; 10 | 11 | if (!__DEV__) { 12 | global.console = { 13 | info: () => {}, 14 | log: () => {}, 15 | warn: () => {}, 16 | debug: () => {}, 17 | error: () => {}, 18 | assert: () => {}, 19 | }; 20 | } 21 | 22 | AppRegistry.registerComponent(appName, () => App); 23 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/launch_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/reducers/system.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-22 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialStore = { 7 | systemData: [], 8 | }; 9 | 10 | const user = (state = initialStore, action) => { 11 | switch (action.type) { 12 | case actionTypes.FETCH_TO_SYSTEM_DATA: 13 | return { 14 | ...state, 15 | systemData: action.systemData, 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default user; 23 | -------------------------------------------------------------------------------- /src/utils/Color.js: -------------------------------------------------------------------------------- 1 | const Color = { 2 | WHITE: '#FFFFFF', 3 | RED: '#FF0000', 4 | TEXT_MAIN: '#222222', 5 | TEXT_DARK: '#666666', 6 | TEXT_LIGHT: '#999999', 7 | THEME: '#2196F3', // 默认APP主题颜色 8 | SPLIT_LINE: '#E8E8E8', // 页面中分割线颜色 9 | DEFAULT_BG: '#f1f2f3', // 所有页面背景颜色 10 | COLLECT: '#FA8072', // 收藏颜色 11 | GOLD: '#FFD700', // 金色 12 | TEXT_TAB_INACTIVE: '#E6E6FA', 13 | WX_GREEN: '#3CB371', 14 | ICON_GRAY: '#E0E0E0', 15 | GUIDE_BG: '#F8F8F8', 16 | }; 17 | 18 | export default Color; 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/reducers/project.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-22 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialStore = { 7 | projectTabs: [], 8 | }; 9 | 10 | const wxArticle = (state = initialStore, action) => { 11 | switch (action.type) { 12 | case actionTypes.FETCH_PROJECT_TABS: 13 | return { 14 | ...state, 15 | projectTabs: action.projectTabs, 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export default wxArticle; 23 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import {combineReducers} from 'redux'; 5 | import home from './home'; 6 | import user from './user'; 7 | import system from './system'; 8 | import wxArticle from './wxArticle'; 9 | import guide from './guide'; 10 | import project from './project'; 11 | import search from './search'; 12 | import collect from './collect'; 13 | import coin from './coin'; 14 | 15 | export default combineReducers({ 16 | home, 17 | user, 18 | system, 19 | wxArticle, 20 | guide, 21 | project, 22 | search, 23 | collect, 24 | coin, 25 | }); 26 | -------------------------------------------------------------------------------- /android/app/build_defs.bzl: -------------------------------------------------------------------------------- 1 | """Helper definitions to glob .aar and .jar targets""" 2 | 3 | def create_aar_targets(aarfiles): 4 | for aarfile in aarfiles: 5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")] 6 | lib_deps.append(":" + name) 7 | android_prebuilt_aar( 8 | name = name, 9 | aar = aarfile, 10 | ) 11 | 12 | def create_jar_targets(jarfiles): 13 | for jarfile in jarfiles: 14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")] 15 | lib_deps.append(":" + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | -------------------------------------------------------------------------------- /src/reducers/guide.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-25 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialStore = { 7 | selectIndex: 0, // 左侧选中的索引值 8 | guideData: [], // 导航数据 9 | }; 10 | 11 | const guide = (state = initialStore, action) => { 12 | switch (action.type) { 13 | case actionTypes.FETCH_GUIDE_DATA: 14 | return { 15 | ...state, 16 | guideData: action.guideData, 17 | }; 18 | case actionTypes.UPDATE_SELECT_INDEX: 19 | return { 20 | ...state, 21 | selectIndex: action.selectIndex, 22 | }; 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | export default guide; 29 | -------------------------------------------------------------------------------- /src/reducers/wxArticle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-22 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialStore = { 7 | articleTabs: [], 8 | isShowLoading: false, 9 | }; 10 | 11 | const wxArticle = (state = initialStore, action) => { 12 | switch (action.type) { 13 | case actionTypes.FETCH_WX_ARTICLE_TABS: 14 | return { 15 | ...state, 16 | articleTabs: action.articleTabs, 17 | }; 18 | case actionTypes.FETCH_ARTICLE_LOADING: 19 | return { 20 | ...state, 21 | isShowLoading: action.isShowLoading, 22 | } 23 | default: 24 | return state; 25 | } 26 | }; 27 | 28 | export default wxArticle; 29 | -------------------------------------------------------------------------------- /ios/RN_WanAndroidTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid-tvOSTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/styles/globalStyles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-20 3 | */ 4 | import {StyleSheet} from 'react-native'; 5 | import Color from '../utils/Color'; 6 | import {DEVICE_WIDTH, getRealDP as dp} from '../utils/screenUtil'; 7 | 8 | export default StyleSheet.create({ 9 | container: { 10 | flex: 1, 11 | backgroundColor: Color.DEFAULT_BG, 12 | }, 13 | splitLine: { 14 | height: dp(1), 15 | width: DEVICE_WIDTH - dp(56), 16 | marginLeft: dp(28), 17 | backgroundColor: Color.SPLIT_LINE, 18 | }, 19 | circleSpecialWrapper: { 20 | width: dp(120), 21 | height: dp(120), 22 | borderRadius: dp(60), 23 | justifyContent: 'center', 24 | alignItems: 'center', 25 | padding: dp(10), 26 | }, 27 | specialText: { 28 | fontSize: dp(20), 29 | color: Color.WHITE, 30 | textAlign: 'center', 31 | fontWeight: 'bold', 32 | lineHeight: dp(23), 33 | }, 34 | articleTitle: { 35 | fontSize: dp(28), 36 | color: Color.TEXT_MAIN, 37 | fontWeight: 'bold', 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /src/utils/httpUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import axios from 'axios'; 5 | import qs from 'querystring'; 6 | 7 | /** 8 | * 网络请求工具类 9 | */ 10 | export default class httpUtil { 11 | static get(url, params) { 12 | return new Promise(async (resolve, reject) => { 13 | try { 14 | let query = await qs.stringify(params); 15 | let res = null; 16 | if (!params) { 17 | res = await axios.get(url); 18 | } else { 19 | res = await axios.get(url + '?' + query); 20 | } 21 | resolve(res); 22 | } catch (error) { 23 | const errorMsg = `请求报错路径: ${url} \n请求报错信息: ${error}`; 24 | console.log(errorMsg); 25 | reject(error); 26 | } 27 | }); 28 | } 29 | 30 | static post(url, params) { 31 | return new Promise(async (resolve, reject) => { 32 | try { 33 | let res = await axios.post(url, params); 34 | resolve(res); 35 | } catch (error) { 36 | const errorMsg = `请求报错路径: ${url} \n请求报错信息: ${error}`; 37 | console.log(errorMsg); 38 | reject(error); 39 | } 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "28.0.3" 6 | minSdkVersion = 16 7 | compileSdkVersion = 28 8 | targetSdkVersion = 28 9 | } 10 | repositories { 11 | google() 12 | jcenter() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:3.4.2") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | maven { 26 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 27 | url("$rootDir/../node_modules/react-native/android") 28 | } 29 | maven { 30 | // Android JSC is installed from npm 31 | url("$rootDir/../node_modules/jsc-android/dist") 32 | } 33 | 34 | google() 35 | jcenter() 36 | maven { url 'https://jitpack.io' } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | APP_RELEASE_STORE_FILE=../keystores/app-key.jks 21 | APP_RELEASE_KEY_ALIAS=appKey 22 | APP_RELEASE_STORE_PASSWORD=rn123456 23 | APP_RELEASE_KEY_PASSWORD=rn123456 24 | 25 | android.useAndroidX=true 26 | android.enableJetifier=true 27 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rn_wanandroid/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.rn_wanandroid; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.facebook.react.ReactActivity; 6 | import com.facebook.react.ReactActivityDelegate; 7 | import com.facebook.react.ReactRootView; 8 | import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; 9 | 10 | import org.devio.rn.splashscreen.SplashScreen; 11 | 12 | public class MainActivity extends ReactActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | SplashScreen.show(this, true); 17 | super.onCreate(savedInstanceState); 18 | } 19 | 20 | /** 21 | * Returns the name of the main component registered from JavaScript. This is used to schedule 22 | * rendering of the component. 23 | */ 24 | @Override 25 | protected String getMainComponentName() { 26 | return "RN_WanAndroid"; 27 | } 28 | 29 | @Override 30 | protected ReactActivityDelegate createReactActivityDelegate() { 31 | return new ReactActivityDelegate(this, getMainComponentName()) { 32 | @Override 33 | protected ReactRootView createRootView() { 34 | return new RNGestureHandlerEnabledRootView(MainActivity.this); 35 | } 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/reducers/coin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-27 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialState = { 7 | page: 1, 8 | coinList: {}, 9 | dataSource: [], // 文章列表数据源 10 | isRenderFooter: false, // 是否渲染列表footer 11 | isFullData: false, // 是否加载完全部数据 12 | coinCount: 0, // 总积分 13 | rank: 0, // 当前排名 14 | }; 15 | 16 | const coin = (state = initialState, action) => { 17 | switch (action.type) { 18 | case actionTypes.FETCH_MY_COIN_LIST: 19 | return { 20 | ...state, 21 | page: 1, 22 | coinList: action.coinList, 23 | dataSource: action.coinList.datas, 24 | isRenderFooter: !!action.coinList.total, // 只有total为0是不渲染footer 25 | isFullData: action.coinList.curPage === action.coinList.pageCount, 26 | }; 27 | case actionTypes.FETCH_MY_COIN_LIST_MORE: 28 | return { 29 | ...state, 30 | page: ++state.page, 31 | dataSource: state.dataSource.concat(action.coinList.datas), 32 | isRenderFooter: true, 33 | isFullData: !action.coinList.datas.length, 34 | }; 35 | case actionTypes.FETCH_MY_COIN_INFO: 36 | return { 37 | ...state, 38 | coinCount: action.coinInfo.coinCount, 39 | rank: action.coinInfo.rank, 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | 46 | export default coin; 47 | -------------------------------------------------------------------------------- /src/utils/AuthUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-20 3 | */ 4 | import StorageUtil from './storageUtil'; 5 | 6 | const userInfoKey = '@userInfo'; 7 | const cookieKey = '@cookie'; 8 | const themeColorKey = '@themeColor'; 9 | const appLanguage = '@appLanguage'; 10 | 11 | class AuthUtil { 12 | static saveAppLanguage = language => { 13 | return StorageUtil.save(appLanguage, language); 14 | }; 15 | 16 | static getAppLanguage = () => { 17 | return StorageUtil.get(appLanguage); 18 | }; 19 | 20 | static saveThemeColor = color => { 21 | return StorageUtil.save(themeColorKey, color); 22 | }; 23 | 24 | static getThemeColor = () => { 25 | return StorageUtil.get(themeColorKey); 26 | }; 27 | 28 | static saveUserInfo = info => { 29 | return StorageUtil.save(userInfoKey, info); 30 | }; 31 | 32 | static getUserInfo = () => { 33 | return StorageUtil.get(userInfoKey); 34 | }; 35 | 36 | static removeUserInfo = () => { 37 | return StorageUtil.delete(userInfoKey); 38 | }; 39 | 40 | static saveCookie(cookie) { 41 | return StorageUtil.save(cookieKey, cookie); 42 | } 43 | 44 | static getCookie = () => { 45 | return StorageUtil.get(cookieKey); 46 | }; 47 | 48 | static removeCookie = () => { 49 | return StorageUtil.delete(cookieKey); 50 | }; 51 | 52 | static removeAllKeys = async () => { 53 | return StorageUtil.delete(await StorageUtil.keys()); 54 | }; 55 | } 56 | 57 | export default AuthUtil; 58 | -------------------------------------------------------------------------------- /src/component/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {ProgressBarAndroid, ProgressViewIOS} from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import {getRealDP as dp, isAndroid} from '../utils/screenUtil'; 5 | import Color from '../utils/Color'; 6 | import {connect} from 'react-redux'; 7 | 8 | const propTypes = { 9 | progress: PropTypes.number.isRequired, 10 | }; 11 | const defaultProps = { 12 | progress: 0, 13 | }; 14 | 15 | /** 16 | * ProgressBar 用于WebView展示时上方进度条 17 | */ 18 | class ProgressBar extends PureComponent { 19 | constructor(props) { 20 | super(props); 21 | this.state = {}; 22 | } 23 | 24 | render() { 25 | const {progress, themeColor} = this.props; 26 | if (progress === 1) { 27 | return null; 28 | } 29 | if (isAndroid) { 30 | return ( 31 | 37 | ); 38 | } 39 | return ( 40 | 45 | ); 46 | } 47 | } 48 | 49 | ProgressBar.propTypes = propTypes; 50 | ProgressBar.defaultProps = defaultProps; 51 | 52 | const mapStateToProps = state => { 53 | return { 54 | themeColor: state.user.themeColor, 55 | }; 56 | }; 57 | 58 | export default connect(mapStateToProps)(ProgressBar); 59 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets") 12 | 13 | lib_deps = [] 14 | 15 | create_aar_targets(glob(["libs/*.aar"])) 16 | 17 | create_jar_targets(glob(["libs/*.jar"])) 18 | 19 | android_library( 20 | name = "all-libs", 21 | exported_deps = lib_deps, 22 | ) 23 | 24 | android_library( 25 | name = "app-code", 26 | srcs = glob([ 27 | "src/main/java/**/*.java", 28 | ]), 29 | deps = [ 30 | ":all-libs", 31 | ":build_config", 32 | ":res", 33 | ], 34 | ) 35 | 36 | android_build_config( 37 | name = "build_config", 38 | package = "com.rn_wanandroid", 39 | ) 40 | 41 | android_resource( 42 | name = "res", 43 | package = "com.rn_wanandroid", 44 | res = "src/main/res", 45 | ) 46 | 47 | android_binary( 48 | name = "app", 49 | keystore = "//android/keystores:debug", 50 | manifest = "src/main/AndroidManifest.xml", 51 | package_type = "debug", 52 | deps = [ 53 | ":app-code", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import "AppDelegate.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 19 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 20 | moduleName:@"RN_WanAndroid" 21 | initialProperties:nil]; 22 | 23 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 24 | 25 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 26 | UIViewController *rootViewController = [UIViewController new]; 27 | rootViewController.view = rootView; 28 | self.window.rootViewController = rootViewController; 29 | [self.window makeKeyAndVisible]; 30 | return YES; 31 | } 32 | 33 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 34 | { 35 | #if DEBUG 36 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 37 | #else 38 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 39 | #endif 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RN_WanAndroid", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "build:android": "cd android && ./gradlew assembleRelease", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "lint": "eslint ." 12 | }, 13 | "dependencies": { 14 | "@react-native-community/async-storage": "^1.6.2", 15 | "axios": "^0.19.0", 16 | "lodash": "^4.17.19", 17 | "querystring": "^0.2.0", 18 | "react": "16.9.0", 19 | "react-native": "0.61.3", 20 | "react-native-gesture-handler": "^1.5.0", 21 | "react-native-i18n": "git+https://github.com/aijason/react-native-i18n", 22 | "react-native-reanimated": "^1.3.2", 23 | "react-native-splash-screen": "^3.2.0", 24 | "react-native-swiper": "^1.6.0-nightly.5", 25 | "react-native-vector-icons": "^6.6.0", 26 | "react-native-webview": "^7.4.3", 27 | "react-navigation": "^4.0.10", 28 | "react-navigation-drawer": "^2.3.3", 29 | "react-navigation-stack": "^1.10.3", 30 | "react-navigation-tabs": "^2.5.6", 31 | "react-redux": "^7.1.1", 32 | "redux": "^4.0.4", 33 | "redux-thunk": "^2.3.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.6.4", 37 | "@babel/runtime": "^7.6.3", 38 | "@react-native-community/eslint-config": "^0.0.5", 39 | "babel-jest": "^24.9.0", 40 | "eslint": "^6.6.0", 41 | "jest": "^24.9.0", 42 | "metro-react-native-babel-preset": "^0.56.3", 43 | "react-test-renderer": "16.9.0" 44 | }, 45 | "jest": { 46 | "preset": "react-native" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/screen/article/WebViewScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {Share, View} from 'react-native'; 3 | import WebView from 'react-native-webview'; 4 | import ProgressBar from '../../component/ProgressBar'; 5 | import globalStyles from '../../styles/globalStyles'; 6 | import NavBar from '../../component/NavBar'; 7 | 8 | /** 9 | * WebViewScreen 10 | */ 11 | class WebViewScreen extends PureComponent { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | progress: 0, 16 | }; 17 | this.onShare = this.onShare.bind(this); 18 | } 19 | 20 | async onShare(title, url) { 21 | try { 22 | await Share.share({ 23 | message: `${title}:${url}`, 24 | }); 25 | } catch (error) { 26 | alert(error.message); 27 | } 28 | } 29 | 30 | render() { 31 | const {navigation} = this.props; 32 | const url = navigation.getParam('url', 'https://www.wanandroid.com/'); 33 | const title = navigation.getParam('title', ''); 34 | return ( 35 | 36 | { 41 | this.onShare(title, url); 42 | }} 43 | /> 44 | 45 | { 48 | this.setState({progress: nativeEvent.progress}); 49 | }} 50 | /> 51 | 52 | ); 53 | } 54 | } 55 | 56 | export default WebViewScreen; 57 | -------------------------------------------------------------------------------- /src/utils/screenUtil.js: -------------------------------------------------------------------------------- 1 | import {PixelRatio, Dimensions, Platform, StatusBar} from 'react-native'; 2 | 3 | export let DEVICE_WIDTH = Dimensions.get('window').width; 4 | export let DEVICE_HEIGHT = Dimensions.get('window').height; 5 | export const isAndroid = Platform.OS === 'android'; 6 | 7 | /** 8 | * 本项目设计基准像素为750 * 1334,使用时视情况调整 9 | * 按比例将设计的px转换成适应不同屏幕的dp 10 | * @param designPx 设计稿标注的px值 11 | * @returns {number} 12 | */ 13 | export function getRealDP(designPx) { 14 | return PixelRatio.roundToNearestPixel((designPx / 750) * DEVICE_WIDTH); 15 | } 16 | 17 | // 是否iphoneX系列(iPhone X, XS, XS Max & XR) 18 | export function isIphoneX() { 19 | const dimen = Dimensions.get('window'); 20 | return ( 21 | Platform.OS === 'ios' && 22 | !Platform.isPad && 23 | !Platform.isTVOS && 24 | (dimen.height === 812 || 25 | dimen.width === 812 || 26 | (dimen.height === 896 || dimen.width === 896)) 27 | ); 28 | } 29 | 30 | // 获取状态栏高度 31 | export function getStatusBarHeight() { 32 | return Platform.select({ 33 | ios: ifIphoneX(44, 20), 34 | android: StatusBar.currentHeight, 35 | }); 36 | } 37 | 38 | // 适配iphoneX屏幕底部距离 39 | export function getBottomSpace() { 40 | return ifIphoneX(34, 0); 41 | } 42 | 43 | /** 44 | * 根据是否是iPhoneX返回不同的样式 45 | * @param iphoneXStyle 46 | * @param iosStyle 47 | * @param androidStyle 48 | * @returns {*} 49 | */ 50 | export function ifIphoneX(iphoneXStyle, iosStyle = {}, androidStyle) { 51 | if (isIphoneX()) { 52 | return iphoneXStyle; 53 | } else if (Platform.OS === 'ios') { 54 | return iosStyle; 55 | } else { 56 | if (androidStyle) { 57 | return androidStyle; 58 | } 59 | return iosStyle; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid-tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSExceptionDomains 28 | 29 | localhost 30 | 31 | NSExceptionAllowsInsecureHTTPLoads 32 | 33 | 34 | 35 | 36 | NSLocationWhenInUseUsageDescription 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/screen/tabs/ProjectScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-16 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View} from 'react-native'; 6 | import {connect} from 'react-redux'; 7 | import NavBar from '../../component/NavBar'; 8 | import globalStyles from '../../styles/globalStyles'; 9 | import {fetchProjectTabs, updateArticleLoading} from '../../actions'; 10 | import ArticleTabComponent from '../../component/ArticleTabComponent'; 11 | import LoadingView from '../../component/LoadingView'; 12 | import {i18n} from '../../utils/Utility'; 13 | 14 | /** 15 | * 项目 16 | */ 17 | class ProjectScreen extends PureComponent { 18 | componentDidMount() { 19 | updateArticleLoading(true); 20 | fetchProjectTabs(); 21 | } 22 | 23 | render() { 24 | const {navigation, projectTabs, isShowLoading} = this.props; 25 | return ( 26 | 27 | navigation.toggleDrawer()} 33 | onRightPress={() => navigation.navigate('Search')} 34 | /> 35 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | const mapStateToProps = state => { 46 | return { 47 | projectTabs: state.project.projectTabs, 48 | isShowLoading: state.wxArticle.isShowLoading, 49 | language: state.user.language, 50 | }; 51 | }; 52 | 53 | export default connect(mapStateToProps)(ProjectScreen); 54 | -------------------------------------------------------------------------------- /src/utils/language/zh-Hant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-16 3 | */ 4 | export default { 5 | 'please-login-first': '請先登錄', 6 | language: '多語言', 7 | 'search-for-more-dry-goods': '搜索更多乾貨', 8 | 'popular-search': '熱門搜索', 9 | 'please-enter-search-keywords': '請輸入搜索關鍵字', 10 | 'about-author': '關於作者', 11 | 'wanAndroid-client-based-on-Facebook-react-native': 12 | '基於Facebook React-Native的玩安卓用戶端', 13 | email: '郵箱', 14 | 'This project is for learning purposes only, not for commercial purposes': 15 | '本項目僅供學習使用,不得用作商業目的', 16 | 'points-details': '積分明細', 17 | 'points-rule': '積分規則', 18 | 'my-collection': '我的收藏', 19 | 'my-points': '我的積分', 20 | 'frequently-used-websites': '常用網站', 21 | 'theme-color': '主題', 22 | 'share-app': '分享', 23 | settings: '設定', 24 | logout: '登出', 25 | 'share-app-desc': 26 | '分享一個使用React Native開發的玩安卓應用,點擊下載:https://github.com/aijason/RN_WanAndroid/releases/download/v1.0.2/RN_WanAndroid-release.apk', 27 | tips: '提示', 28 | cancel: '取消', 29 | confirm: '確認', 30 | 'Are you sure to log out': '確認退出登录嗎', 31 | 'not-logged-in': '未登錄', 32 | 'set-up-themes': '設定主題', 33 | 'User name cannot be empty': '用戶名不能為空', 34 | 'Password cannot be empty': '密碼不能為空', 35 | 'Confirm password cannot be empty': '確認密碼不能為空', 36 | login: '登入', 37 | userName: '用戶名', 38 | password: '密碼', 39 | 'New users? To': '新用戶?去 ', 40 | register: '注册', 41 | confirmPassword: '確認密碼', 42 | 'Playing with life loading': '玩命加載中...', 43 | Navigation: '導航', 44 | wanAndroid: '玩安卓', 45 | project: '項目', 46 | system: '體系', 47 | publicAccount: '公眾號', 48 | Loading: '正在加載中...', 49 | 'Have been collected': '已收藏', 50 | Article: '文章', 51 | home: '首頁', 52 | 'all-loaded': '已加載全部', 53 | }; 54 | -------------------------------------------------------------------------------- /src/utils/language/zh-Hans.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-16 3 | */ 4 | export default { 5 | 'please-login-first': '请先登录', 6 | language: '多语言', 7 | 'search-for-more-dry-goods': '搜索更多干货', 8 | 'popular-search': '热门搜索', 9 | 'please-enter-search-keywords': '请输入搜索关键词', 10 | 'about-author': '关于作者', 11 | 'wanAndroid-client-based-on-Facebook-react-native': 12 | '基于Facebook React-Native的玩安卓客户端', 13 | email: '邮箱', 14 | 'This project is for learning purposes only, not for commercial purposes': 15 | '本项目仅供学习使用,不得用作商业目的', 16 | 'points-details': '积分明细', 17 | 'points-rule': '积分规则', 18 | 'my-collection': '我的收藏', 19 | 'my-points': '我的积分', 20 | 'frequently-used-websites': '常用网站', 21 | 'theme-color': '主题', 22 | 'share-app': '分享', 23 | settings: '设置', 24 | logout: '退出登录', 25 | 'share-app-desc': 26 | '分享一个使用React Native开发的玩安卓应用, 点击下载:https://github.com/aijason/RN_WanAndroid/releases/download/v1.0.2/RN_WanAndroid-release.apk', 27 | tips: '提示', 28 | cancel: '取消', 29 | confirm: '确认', 30 | 'Are you sure to log out': '确认退出登录吗', 31 | 'not-logged-in': '未登录', 32 | 'set-up-themes': '设置主题', 33 | 'User name cannot be empty': '用户名不能为空', 34 | 'Password cannot be empty': '密码不能为空', 35 | 'Confirm password cannot be empty': '确认密码不能为空', 36 | login: '登录', 37 | userName: '用户名', 38 | password: '密码', 39 | 'New users? To': '新用户? 去 ', 40 | register: '注册', 41 | confirmPassword: '确认密码', 42 | 'Playing with life loading': '玩命加载中...', 43 | Navigation: '导航', 44 | wanAndroid: '玩安卓', 45 | project: '项目', 46 | system: '体系', 47 | publicAccount: '公众号', 48 | Loading: '正在加载中...', 49 | 'Have been collected': '已收藏', 50 | Article: '文章', 51 | home: '首页', 52 | 'all-loaded': '已加载全部', 53 | }; 54 | -------------------------------------------------------------------------------- /src/component/ListFooter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-20 3 | */ 4 | import React, {Component} from 'react'; 5 | import {Text, View, StyleSheet, ActivityIndicator} from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | import {DEVICE_WIDTH, getRealDP as dp} from '../utils/screenUtil'; 8 | import Color from '../utils/Color'; 9 | import {i18n} from '../utils/Utility'; 10 | 11 | const propTypes = { 12 | isRenderFooter: PropTypes.bool.isRequired, 13 | isFullData: PropTypes.bool.isRequired, 14 | indicatorColor: PropTypes.string, 15 | }; 16 | 17 | const defaultProps = { 18 | isRenderFooter: false, 19 | isFullData: false, 20 | indicatorColor: Color.THEME, 21 | }; 22 | 23 | class ListFooter extends Component { 24 | render() { 25 | if (!this.props.isRenderFooter) { 26 | return null; 27 | } 28 | if (this.props.isFullData) { 29 | return ( 30 | 31 | {i18n('all-loaded')} 32 | 33 | ); 34 | } else { 35 | return ( 36 | 37 | 38 | 39 | {i18n('Playing with life loading')} 40 | 41 | 42 | ); 43 | } 44 | } 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | footer: { 49 | flexDirection: 'row', 50 | width: DEVICE_WIDTH, 51 | height: dp(80), 52 | alignItems: 'center', 53 | justifyContent: 'center', 54 | }, 55 | }); 56 | 57 | ListFooter.propTypes = propTypes; 58 | ListFooter.defaultProps = defaultProps; 59 | 60 | export default ListFooter; 61 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-20 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | import Color from '../utils/Color'; 6 | 7 | const initialStore = { 8 | isLogin: false, // 是否已登录 9 | userInfo: { 10 | admin: false, 11 | chapterTops: [], 12 | collectIds: [], 13 | email: '', 14 | icon: '', 15 | id: '', 16 | nickname: '', 17 | password: '', 18 | publicName: '', 19 | token: '', 20 | type: '', 21 | username: '', 22 | }, // 用户信息 23 | themeColor: Color.THEME, // 用户设置APP主题色 24 | language: 'zhHans', // APP默认语言 25 | }; 26 | 27 | const user = (state = initialStore, action) => { 28 | switch (action.type) { 29 | case actionTypes.FETCH_TO_REGISTER: 30 | return { 31 | ...state, 32 | isLogin: false, 33 | }; 34 | case actionTypes.FETCH_TO_LOGIN: 35 | return { 36 | ...state, 37 | isLogin: true, 38 | userInfo: action.userInfo, 39 | }; 40 | case actionTypes.FETCH_TO_LOGIN_FAILURE: 41 | return { 42 | ...state, 43 | isLogin: false, 44 | }; 45 | case actionTypes.FETCH_TO_LOGOUT: 46 | return { 47 | ...state, 48 | isLogin: false, 49 | }; 50 | case actionTypes.CHANGE_THEME_COLOR: 51 | return { 52 | ...state, 53 | themeColor: action.themeColor, 54 | }; 55 | case actionTypes.INITIAL_AUTH_INFO: 56 | return { 57 | ...state, 58 | ...action.initialInfo, 59 | }; 60 | case actionTypes.SWITCH_APP_LANGUAGE: 61 | return { 62 | ...state, 63 | language: action.language, 64 | }; 65 | default: 66 | return state; 67 | } 68 | }; 69 | 70 | export default user; 71 | -------------------------------------------------------------------------------- /src/reducers/collect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-27 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialState = { 7 | page: 1, // 第一页page为0 8 | collectData: {}, 9 | dataSource: [], // 文章列表数据源 10 | isRenderFooter: false, // 是否渲染列表footer 11 | isFullData: false, // 是否加载完全部数据 12 | }; 13 | 14 | const collect = (state = initialState, action) => { 15 | switch (action.type) { 16 | case actionTypes.FETCH_COLLECT_ARTICLE: 17 | return { 18 | ...state, 19 | page: 1, 20 | collectData: action.collectData, 21 | dataSource: action.collectData.datas, 22 | isRenderFooter: !!action.collectData.total, // 只有total为0是不渲染footer 23 | isFullData: action.collectData.curPage === action.collectData.pageCount, 24 | }; 25 | case actionTypes.FETCH_COLLECT_ARTICLE_MORE: 26 | return { 27 | ...state, 28 | page: ++state.page, 29 | dataSource: state.dataSource.concat(action.collectData.datas), 30 | isRenderFooter: true, 31 | isFullData: !action.collectData.datas.length, 32 | }; 33 | case actionTypes.FETCH_MYCOLLECT_ADD_COLLECT: 34 | let addCollectDataSource = [...state.dataSource]; 35 | addCollectDataSource[action.index].collect = true; 36 | return { 37 | ...state, 38 | dataSource: addCollectDataSource, 39 | }; 40 | case actionTypes.FETCH_MYCOLLECT_CANCEL_COLLECT: 41 | let cancelCollectDataSource = [...state.dataSource]; 42 | cancelCollectDataSource[action.index].collect = false; 43 | return { 44 | ...state, 45 | dataSource: cancelCollectDataSource, 46 | }; 47 | default: 48 | return state; 49 | } 50 | }; 51 | 52 | export default collect; 53 | -------------------------------------------------------------------------------- /src/screen/article/ArticleTabScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {View} from 'react-native'; 3 | import globalStyles from '../../styles/globalStyles'; 4 | import NavBar from '../../component/NavBar'; 5 | import ArticleTabComponent from '../../component/ArticleTabComponent'; 6 | import {updateArticleLoading} from '../../actions'; 7 | import LoadingView from '../../component/LoadingView'; 8 | import {connect} from 'react-redux'; 9 | 10 | /** 11 | * ArticleTabScreen 12 | */ 13 | class ArticleTabScreen extends PureComponent { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | articleTabs: [], 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | const {navigation} = this.props; 23 | updateArticleLoading(true); 24 | const articleTabs = navigation.getParam('articleTabs', []); 25 | this.timer && clearTimeout(this.timer); 26 | this.timer = setTimeout(() => { 27 | this.setState({articleTabs}); 28 | }, 100); 29 | } 30 | 31 | componentWillUnmount() { 32 | this.timer && clearTimeout(this.timer); 33 | } 34 | 35 | render() { 36 | const {navigation, isShowLoading} = this.props; 37 | const title = navigation.getParam('title', ''); 38 | return ( 39 | 40 | 41 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | const mapStateToProps = state => { 52 | return { 53 | isShowLoading: state.wxArticle.isShowLoading, 54 | }; 55 | }; 56 | 57 | export default connect(mapStateToProps)(ArticleTabScreen); 58 | -------------------------------------------------------------------------------- /src/screen/tabs/WxArticleScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-16 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View} from 'react-native'; 6 | import {connect} from 'react-redux'; 7 | import NavBar from '../../component/NavBar'; 8 | import Color from '../../utils/Color'; 9 | import globalStyles from '../../styles/globalStyles'; 10 | import {fetchWxArticleTabs, updateArticleLoading} from '../../actions'; 11 | import ArticleTabComponent from '../../component/ArticleTabComponent'; 12 | import LoadingView from '../../component/LoadingView'; 13 | import {i18n} from '../../utils/Utility'; 14 | 15 | /** 16 | * 微信公众号 17 | */ 18 | class WxArticleScreen extends PureComponent { 19 | constructor(props) { 20 | super(props); 21 | this.state = {}; 22 | } 23 | 24 | componentDidMount() { 25 | updateArticleLoading(true); 26 | fetchWxArticleTabs(); 27 | } 28 | 29 | render() { 30 | const {navigation, articleTabs, isShowLoading} = this.props; 31 | return ( 32 | 33 | navigation.toggleDrawer()} 39 | onRightPress={() => navigation.navigate('Search')} 40 | /> 41 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | const mapStateToProps = state => { 53 | return { 54 | articleTabs: state.wxArticle.articleTabs, 55 | isShowLoading: state.wxArticle.isShowLoading, 56 | language: state.user.language, 57 | }; 58 | }; 59 | 60 | export default connect(mapStateToProps)(WxArticleScreen); 61 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {StatusBar, View} from 'react-native'; 3 | import {Provider} from 'react-redux'; 4 | import SplashScreen from 'react-native-splash-screen'; 5 | import {pickBy, identity} from 'lodash'; 6 | import AppNavigator from './src'; 7 | import {setAxios} from './src/service/setAxios'; 8 | import store from './src/store'; 9 | import AuthUtil from './src/utils/AuthUtil'; 10 | import {switchAPPLanguage, toInitialAuthInfo} from './src/actions'; 11 | import Toast from './src/utils/Toast'; 12 | 13 | class App extends PureComponent { 14 | constructor(props) { 15 | super(props); 16 | this.state = {}; 17 | this.initialInfo = this.initialInfo.bind(this); 18 | } 19 | 20 | static async getDerivedStateFromProps() { 21 | const language = await AuthUtil.getAppLanguage(); 22 | if (language) { 23 | await switchAPPLanguage(language); // 设置app语言环境 24 | } else { 25 | await switchAPPLanguage('zhHans'); // 默认中文 26 | } 27 | } 28 | 29 | componentDidMount() { 30 | SplashScreen.hide(); 31 | setAxios(); // 网络设置 32 | global.toast = this.refs.toast; // 全局引用赋值 33 | this.initialInfo(); 34 | } 35 | 36 | async initialInfo() { 37 | const userInfo = await AuthUtil.getUserInfo(); 38 | const themeColor = await AuthUtil.getThemeColor(); 39 | const authInfo = pickBy( 40 | { 41 | isLogin: !!userInfo, 42 | userInfo, 43 | themeColor, 44 | }, 45 | identity, 46 | ); 47 | if (Object.keys(authInfo).length === 0) { 48 | return; 49 | } 50 | console.log('初始化缓存信息', authInfo); 51 | toInitialAuthInfo(authInfo); 52 | } 53 | 54 | render() { 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /src/component/LoadingView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-04 3 | */ 4 | 5 | import React, {Component} from 'react'; 6 | import {StyleSheet, Text, View, ActivityIndicator} from 'react-native'; 7 | import PropTypes from 'prop-types'; 8 | import { 9 | DEVICE_HEIGHT, 10 | DEVICE_WIDTH, 11 | getRealDP as dp, 12 | } from '../utils/screenUtil'; 13 | import Color from '../utils/Color'; 14 | import {i18n} from '../utils/Utility'; 15 | 16 | const propTypes = { 17 | isShowLoading: PropTypes.bool.isRequired, 18 | }; 19 | 20 | const defaultProps = { 21 | isShowLoading: false, 22 | }; 23 | 24 | class LoadingView extends Component { 25 | render() { 26 | const {isShowLoading} = this.props; 27 | if (!isShowLoading) { 28 | return null; 29 | } 30 | return ( 31 | 32 | 33 | 34 | 35 | {i18n('Loading')} 36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | const styles = StyleSheet.create({ 44 | loadingContainer: { 45 | position: 'absolute', 46 | left: 0, 47 | top: 0, 48 | backgroundColor: 'transparent', 49 | width: DEVICE_WIDTH, 50 | height: DEVICE_HEIGHT, 51 | justifyContent: 'center', 52 | alignItems: 'center', 53 | }, 54 | loadingWrapper: { 55 | width: dp(200), 56 | height: dp(200), 57 | backgroundColor: 'rgba(0,0,0,0.05)', 58 | }, 59 | loadingContent: { 60 | width: dp(200), 61 | height: dp(200), 62 | backgroundColor: 'rgba(0,0,0,0.6)', 63 | justifyContent: 'center', 64 | alignItems: 'center', 65 | borderRadius: 7, 66 | }, 67 | loadingText: { 68 | marginLeft: dp(20), 69 | color: Color.WHITE, 70 | marginTop: dp(20), 71 | fontSize: dp(28), 72 | }, 73 | }); 74 | 75 | LoadingView.propTypes = propTypes; 76 | LoadingView.defaultProps = defaultProps; 77 | 78 | export default LoadingView; 79 | -------------------------------------------------------------------------------- /src/reducers/search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-27 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialState = { 7 | page: 1, // 第一页page为0 8 | hotKey: [], // 搜索热词 9 | articleData: {}, 10 | dataSource: [], // 文章列表数据源 11 | isRenderFooter: false, // 是否渲染列表footer 12 | isFullData: false, // 是否加载完全部数据 13 | }; 14 | 15 | const search = (state = initialState, action) => { 16 | switch (action.type) { 17 | case actionTypes.FETCH_SEARCH_HOT_KEY: 18 | return { 19 | ...state, 20 | hotKey: action.hotKey, 21 | }; 22 | case actionTypes.FETCH_SEARCH_ARTICLE: 23 | return { 24 | ...state, 25 | page: 1, 26 | articleData: action.articleData, 27 | dataSource: action.articleData.datas, 28 | isRenderFooter: !!action.articleData.total, // 只有total为0是不渲染footer 29 | isFullData: action.articleData.curPage === action.articleData.pageCount, 30 | }; 31 | case actionTypes.FETCH_SEARCH_ARTICLE_MORE: 32 | return { 33 | ...state, 34 | page: ++state.page, 35 | dataSource: state.dataSource.concat(action.articleData.datas), 36 | isRenderFooter: true, 37 | isFullData: !action.articleData.datas.length, 38 | }; 39 | case actionTypes.CLEAR_SEARCH_ARTICLE: 40 | return { 41 | ...state, 42 | page: 1, 43 | articleData: {}, 44 | dataSource: [], 45 | isRenderFooter: false, 46 | isFullData: false, 47 | }; 48 | case actionTypes.FETCH_SEARCH_ARTICLE_ADD_COLLECT: 49 | let addCollectDataSource = [...state.dataSource]; 50 | addCollectDataSource[action.index].collect = true; 51 | return { 52 | ...state, 53 | dataSource: addCollectDataSource, 54 | }; 55 | case actionTypes.FETCH_SEARCH_ARTICLE_CANCEL_COLLECT: 56 | let cancelCollectDataSource = [...state.dataSource]; 57 | cancelCollectDataSource[action.index].collect = false; 58 | return { 59 | ...state, 60 | dataSource: cancelCollectDataSource, 61 | }; 62 | default: 63 | return state; 64 | } 65 | }; 66 | 67 | export default search; 68 | -------------------------------------------------------------------------------- /src/reducers/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import actionTypes from '../actions/actionType'; 5 | 6 | const initialStore = { 7 | page: 1, // 第一页page为0 8 | dataSource: [], // 列表数据源 9 | homeList: {}, 10 | homeBanner: [], // 首页轮播数据 11 | isRenderFooter: false, // 是否渲染列表footer 12 | isFullData: false, // 是否加载完全部数据 13 | websites: [], // 常用网站数据 14 | }; 15 | 16 | const home = (state = initialStore, action) => { 17 | switch (action.type) { 18 | case actionTypes.FETCH_HOME_BANNER: 19 | return { 20 | ...state, 21 | homeBanner: action.homeBanner, 22 | }; 23 | case actionTypes.FETCH_HOME_LIST: 24 | return { 25 | ...state, 26 | page: 1, 27 | homeList: action.homeList, 28 | dataSource: action.homeList.datas, 29 | isRenderFooter: !!action.homeList.total, // 只有total为0是不渲染footer 30 | isFullData: action.homeList.curPage === action.homeList.pageCount, // 数据最后一页显示"已加载全部" 31 | }; 32 | case actionTypes.FETCH_HOME_LIST_FAILURE: 33 | return state; 34 | case actionTypes.FETCH_HOME_LIST_MORE: 35 | return { 36 | ...state, 37 | page: ++state.page, 38 | dataSource: state.dataSource.concat(action.homeList.datas), 39 | isRenderFooter: !!action.homeList.total, // 只有total为0是不渲染footer 40 | isFullData: action.homeList.datas.length === 0, 41 | }; 42 | case actionTypes.FETCH_OFTEN_USED_WEBSITES: 43 | return { 44 | ...state, 45 | websites: action.websites, 46 | }; 47 | case actionTypes.FETCH_HOME_ADD_COLLECT: 48 | let addCollectDataSource = [...state.dataSource]; 49 | addCollectDataSource[action.index].collect = true; 50 | return { 51 | ...state, 52 | dataSource: addCollectDataSource, 53 | }; 54 | case actionTypes.FETCH_HOME_CANCEL_COLLECT: 55 | let cancelCollectDataSource = [...state.dataSource]; 56 | cancelCollectDataSource[action.index].collect = false; 57 | return { 58 | ...state, 59 | dataSource: cancelCollectDataSource, 60 | }; 61 | default: 62 | return state; 63 | } 64 | }; 65 | 66 | export default home; 67 | -------------------------------------------------------------------------------- /src/utils/language/en.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-16 3 | */ 4 | export default { 5 | 'please-login-first': 'Please login first', 6 | language: 'Language', 7 | 'search-for-more-dry-goods': 'Search for more dry goods', 8 | 'popular-search': 'Popular search', 9 | 'please-enter-search-keywords': 'Please enter search keywords', 10 | 'about-author': 'About author', 11 | 'wanAndroid-client-based-on-Facebook-react-native': 12 | 'wanAndroid client based on Facebook react native', 13 | email: 'Email', 14 | 'This project is for learning purposes only, not for commercial purposes': 15 | 'This project is for learning purposes only, not for commercial purposes', 16 | 'points-details': 'Points details', 17 | 'points-rule': 'Points rule', 18 | 'my-collection': 'My collection', 19 | 'my-points': 'My points', 20 | 'frequently-used-websites': 'Frequently used websites', 21 | 'theme-color': 'Theme', 22 | 'share-app': 'Share', 23 | settings: 'Settings', 24 | logout: 'Logout', 25 | 'share-app-desc': 26 | 'Share WanAndroid application developed with react native,Click to download:https://github.com/aijason/RN_WanAndroid/releases/download/v1.0.2/RN_WanAndroid-release.apk', 27 | tips: 'Tips', 28 | cancel: 'Cancel', 29 | confirm: 'Confirm', 30 | 'Are you sure to log out': 'Are you sure to log out', 31 | 'not-logged-in': 'Not logged in', 32 | 'set-up-themes': 'set up themes', 33 | 'User name cannot be empty': 'User name cannot be empty', 34 | 'Password cannot be empty': 'Password cannot be empty', 35 | 'Confirm password cannot be empty': 'Confirm password cannot be empty', 36 | login: 'Log In', 37 | userName: 'User name', 38 | password: 'Password', 39 | 'New users? To': 'New users? To ', 40 | register: 'register', 41 | confirmPassword: 'Confirm password', 42 | 'Playing with life loading': 'Playing with life loading...', 43 | Navigation: 'Navigation', 44 | wanAndroid: 'WanAndroid', 45 | project: 'Project', 46 | system: 'System', 47 | publicAccount: 'PublicAccount', 48 | Loading: 'Loading...', 49 | 'Have been collected': 'Have been collected', 50 | Article: 'Article', 51 | home: 'Home', 52 | 'all-loaded': 'All loaded', 53 | }; 54 | -------------------------------------------------------------------------------- /ios/RN_WanAndroidTests/RN_WanAndroidTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #define TIMEOUT_SECONDS 600 15 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 16 | 17 | @interface RN_WanAndroidTests : XCTestCase 18 | 19 | @end 20 | 21 | @implementation RN_WanAndroidTests 22 | 23 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 24 | { 25 | if (test(view)) { 26 | return YES; 27 | } 28 | for (UIView *subview in [view subviews]) { 29 | if ([self findSubviewInView:subview matching:test]) { 30 | return YES; 31 | } 32 | } 33 | return NO; 34 | } 35 | 36 | - (void)testRendersWelcomeScreen 37 | { 38 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 39 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 40 | BOOL foundElement = NO; 41 | 42 | __block NSString *redboxError = nil; 43 | #ifdef DEBUG 44 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 45 | if (level >= RCTLogLevelError) { 46 | redboxError = message; 47 | } 48 | }); 49 | #endif 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | #ifdef DEBUG 64 | RCTSetLogFunction(RCTDefaultLogFunction); 65 | #endif 66 | 67 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 68 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 69 | } 70 | 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore polyfills 9 | node_modules/react-native/Libraries/polyfills/.* 10 | 11 | ; These should not be required directly 12 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 13 | node_modules/warning/.* 14 | 15 | ; Flow doesn't support platforms 16 | .*/Libraries/Utilities/LoadingView.js 17 | 18 | [untyped] 19 | .*/node_modules/@react-native-community/cli/.*/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/Libraries/react-native/react-native-interface.js 25 | node_modules/react-native/flow/ 26 | 27 | [options] 28 | emoji=true 29 | 30 | esproposal.optional_chaining=enable 31 | esproposal.nullish_coalescing=enable 32 | 33 | module.file_ext=.js 34 | module.file_ext=.json 35 | module.file_ext=.ios.js 36 | 37 | munge_underscores=true 38 | 39 | module.name_mapper='^react-native$' -> '/node_modules/react-native/Libraries/react-native/react-native-implementation' 40 | module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' 41 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' 42 | 43 | suppress_type=$FlowIssue 44 | suppress_type=$FlowFixMe 45 | suppress_type=$FlowFixMeProps 46 | suppress_type=$FlowFixMeState 47 | 48 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 49 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 50 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 51 | 52 | [lints] 53 | sketchy-null-number=warn 54 | sketchy-null-mixed=warn 55 | sketchy-number=warn 56 | untyped-type-import=warn 57 | nonstrict-import=warn 58 | deprecated-type=warn 59 | unsafe-getters-setters=warn 60 | inexact-spread=warn 61 | unnecessary-invariant=warn 62 | signature-verification-failure=warn 63 | deprecated-utility=error 64 | 65 | [strict] 66 | deprecated-type 67 | nonstrict-import 68 | sketchy-null 69 | unclear-type 70 | unsafe-getters-setters 71 | untyped-import 72 | untyped-type-import 73 | 74 | [version] 75 | ^0.105.0 76 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | RN_WanAndroid 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSExceptionDomains 32 | 33 | localhost 34 | 35 | NSExceptionAllowsInsecureHTTPLoads 36 | 37 | 38 | 39 | 40 | NSLocationWhenInUseUsageDescription 41 | 42 | UIAppFonts 43 | 44 | AntDesign.ttf 45 | Entypo.ttf 46 | EvilIcons.ttf 47 | Feather.ttf 48 | FontAwesome.ttf 49 | FontAwesome5_Brands.ttf 50 | FontAwesome5_Regular.ttf 51 | FontAwesome5_Solid.ttf 52 | Fontisto.ttf 53 | Foundation.ttf 54 | Ionicons.ttf 55 | MaterialCommunityIcons.ttf 56 | MaterialIcons.ttf 57 | Octicons.ttf 58 | SimpleLineIcons.ttf 59 | Zocial.ttf 60 | 61 | UIRequiredDeviceCapabilities 62 | 63 | armv7 64 | 65 | UISupportedInterfaceOrientations 66 | 67 | UIInterfaceOrientationPortrait 68 | UIInterfaceOrientationLandscapeLeft 69 | UIInterfaceOrientationLandscapeRight 70 | 71 | UIViewControllerBasedStatusBarAppearance 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/screen/drawer/LanguageScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-16 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {StyleSheet, Text, View} from 'react-native'; 6 | import Color from '../../utils/Color'; 7 | import NavBar from '../../component/NavBar'; 8 | import globalStyles from '../../styles/globalStyles'; 9 | import {getRealDP as dp} from '../../utils/screenUtil'; 10 | import {connect} from 'react-redux'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import Touchable from '../../component/Touchable'; 13 | import {switchAPPLanguage} from '../../actions'; 14 | import {getLanguageList, i18n} from '../../utils/Utility'; 15 | 16 | /** 17 | * 多语言设置 18 | */ 19 | class LanguageScreen extends PureComponent { 20 | constructor(props) { 21 | super(props); 22 | this.state = {}; 23 | this.renderLanguageItem = this.renderLanguageItem.bind(this); 24 | } 25 | 26 | renderLanguageItem(item) { 27 | const {language} = this.props; 28 | return ( 29 | switchAPPLanguage(item.languageCode)}> 34 | 35 | {item.language} 36 | {language === item.languageCode ? ( 37 | 38 | ) : null} 39 | 40 | 41 | ); 42 | } 43 | 44 | render() { 45 | const {navigation} = this.props; 46 | return ( 47 | 48 | 49 | {getLanguageList().map(el => this.renderLanguageItem(el))} 50 | 51 | ); 52 | } 53 | } 54 | 55 | const styles = StyleSheet.create({ 56 | itemWrapper: { 57 | height: dp(120), 58 | flexDirection: 'row', 59 | justifyContent: 'space-between', 60 | alignItems: 'center', 61 | paddingHorizontal: dp(28), 62 | backgroundColor: Color.WHITE, 63 | borderBottomWidth: dp(1), 64 | borderBottomColor: Color.SPLIT_LINE, 65 | }, 66 | languageText: { 67 | fontSize: dp(30), 68 | color: Color.TEXT_MAIN, 69 | }, 70 | }); 71 | 72 | const mapStateToProps = state => { 73 | return { 74 | language: state.user.language, 75 | }; 76 | }; 77 | 78 | export default connect(mapStateToProps)(LanguageScreen); 79 | -------------------------------------------------------------------------------- /src/actions/actionType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | 5 | const actionTypes = { 6 | // HOME 7 | FETCH_HOME_BANNER: 'FETCH_HOME_BANNER', // 请求首页轮播 8 | FETCH_HOME_LIST: 'FETCH_HOME_LIST', // 请求首页列表 9 | FETCH_HOME_LIST_FAILURE: 'FETCH_HOME_LIST_FAILURE', // 请求首页列表失败 10 | FETCH_HOME_LIST_MORE: 'FETCH_HOME_LIST_MORE', // 请求首页列表上拉加载更多 11 | FETCH_OFTEN_USED_WEBSITES: 'FETCH_OFTEN_USED_WEBSITES', // 请求常用网站数据 12 | FETCH_HOME_ADD_COLLECT: 'FETCH_HOME_ADD_COLLECT', // 请求收藏首页文章 13 | FETCH_HOME_CANCEL_COLLECT: 'FETCH_HOME_CANCEL_COLLECT', // 取消收藏首页文章 14 | // USER 15 | FETCH_TO_REGISTER: 'FETCH_TO_REGISTER', // 请求注册 16 | FETCH_TO_REGISTER_FAILURE: 'FETCH_TO_REGISTER_FAILURE', // 请求注册失败 17 | FETCH_TO_LOGIN: 'FETCH_TO_LOGIN', // 请求登录 18 | FETCH_TO_LOGIN_FAILURE: 'FETCH_TO_LOGIN_FAILURE', // 请求登录失败 19 | FETCH_TO_LOGOUT: 'FETCH_TO_LOGOUT', // 请求登出 20 | CHANGE_THEME_COLOR: 'CHANGE_THEME_COLOR', // 改变应用主题色 21 | INITIAL_AUTH_INFO: 'INITIAL_AUTH_INFO', // 初始化个人设置信息 22 | SWITCH_APP_LANGUAGE: 'SWITCH_APP_LANGUAGE', // 切换应用语言 23 | // SYSTEM 24 | FETCH_TO_SYSTEM_DATA: 'FETCH_TO_SYSTEM_DATA', // 请求体系数据 25 | // WX_ARTICLE 26 | FETCH_WX_ARTICLE_TABS: 'FETCH_WX_ARTICLE_TABS', // 请求公众号Tab列表 27 | FETCH_WX_ARTICLE_LIST: 'FETCH_WX_ARTICLE_LIST', // 请求某个公众号下的列表 28 | FETCH_ARTICLE_LOADING: 'FETCH_ARTICLE_LOADING', // Article loading 29 | // GUIDE 30 | FETCH_GUIDE_DATA: 'FETCH_GUIDE_DATA', // 请求导航数据 31 | UPDATE_SELECT_INDEX: 'UPDATE_SELECT_INDEX', // 更新选中索引值 32 | // PROJECT 33 | FETCH_PROJECT_TABS: 'FETCH_PROJECT_TABS', // 获取项目分类 34 | // SEARCH 35 | FETCH_SEARCH_HOT_KEY: 'FETCH_SEARCH_HOT_KEY', // 获取搜索热词 36 | FETCH_SEARCH_ARTICLE: 'FETCH_SEARCH_ARTICLE', // 根据关键词搜索文章 37 | FETCH_SEARCH_ARTICLE_MORE: 'FETCH_SEARCH_ARTICLE_MORE', // 根据关键词搜索文章列表上拉加载更多 38 | CLEAR_SEARCH_ARTICLE: 'CLEAR_SEARCH_ARTICLE', // 清空上一次加载记录 39 | FETCH_SEARCH_ARTICLE_ADD_COLLECT: 'FETCH_SEARCH_ARTICLE_ADD_COLLECT', // 搜索页收藏 40 | FETCH_SEARCH_ARTICLE_CANCEL_COLLECT: 'FETCH_SEARCH_ARTICLE_CANCEL_COLLECT', // 搜索页取消收藏 41 | // COLLECT 42 | FETCH_COLLECT_ARTICLE: 'FETCH_COLLECT_ARTICLE', // 请求收藏文章列表 43 | FETCH_COLLECT_ARTICLE_MORE: 'FETCH_COLLECT_ARTICLE_MORE', // 请求收藏文章列表上拉加载更多 44 | FETCH_MYCOLLECT_CANCEL_COLLECT: 'FETCH_MYCOLLECT_CANCEL_COLLECT', // 我的收藏页面取消收藏 45 | FETCH_MYCOLLECT_ADD_COLLECT: 'FETCH_MYCOLLECT_ADD_COLLECT', // 我的收藏页面收藏 46 | // COIN 47 | FETCH_MY_COIN_LIST: 'FETCH_MY_COIN_LIST', // 请求我的积分明细列表 48 | FETCH_MY_COIN_LIST_MORE: 'FETCH_MY_COIN_LIST_MORE', // 请求我的积分明细列表上拉加载更多 49 | FETCH_MY_COIN_INFO: 'FETCH_MY_COIN_INFO', // 获取个人积分,需要登录后访问 50 | }; 51 | 52 | export default actionTypes; 53 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/rn_wanandroid/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.rn_wanandroid; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import com.facebook.react.PackageList; 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.soloader.SoLoader; 10 | import java.lang.reflect.InvocationTargetException; 11 | import java.util.List; 12 | 13 | public class MainApplication extends Application implements ReactApplication { 14 | 15 | private final ReactNativeHost mReactNativeHost = 16 | new ReactNativeHost(this) { 17 | @Override 18 | public boolean getUseDeveloperSupport() { 19 | return BuildConfig.DEBUG; 20 | } 21 | 22 | @Override 23 | protected List getPackages() { 24 | @SuppressWarnings("UnnecessaryLocalVariable") 25 | List packages = new PackageList(this).getPackages(); 26 | // Packages that cannot be autolinked yet can be added manually here, for example: 27 | // packages.add(new MyReactNativePackage()); 28 | return packages; 29 | } 30 | 31 | @Override 32 | protected String getJSMainModuleName() { 33 | return "index"; 34 | } 35 | }; 36 | 37 | @Override 38 | public ReactNativeHost getReactNativeHost() { 39 | return mReactNativeHost; 40 | } 41 | 42 | @Override 43 | public void onCreate() { 44 | super.onCreate(); 45 | SoLoader.init(this, /* native exopackage */ false); 46 | initializeFlipper(this); // Remove this line if you don't want Flipper enabled 47 | } 48 | 49 | /** 50 | * Loads Flipper in React Native templates. 51 | * 52 | * @param context 53 | */ 54 | private static void initializeFlipper(Context context) { 55 | if (BuildConfig.DEBUG) { 56 | try { 57 | /* 58 | We use reflection here to pick up the class that initializes Flipper, 59 | since Flipper library is not available in release mode 60 | */ 61 | Class aClass = Class.forName("com.facebook.flipper.ReactNativeFlipper"); 62 | aClass.getMethod("initializeFlipper", Context.class).invoke(null, context); 63 | } catch (ClassNotFoundException e) { 64 | e.printStackTrace(); 65 | } catch (NoSuchMethodException e) { 66 | e.printStackTrace(); 67 | } catch (IllegalAccessException e) { 68 | e.printStackTrace(); 69 | } catch (InvocationTargetException e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/component/ArticleTabComponent.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {createAppContainer} from 'react-navigation'; 3 | import PropTypes from 'prop-types'; 4 | import {createMaterialTopTabNavigator} from 'react-navigation-tabs'; 5 | import Color from '../utils/Color'; 6 | import {getRealDP as dp} from '../utils/screenUtil'; 7 | import ArticleFlatList from './ArticleFlatList'; 8 | import {connect} from 'react-redux'; 9 | 10 | const propTypes = { 11 | articleTabs: PropTypes.array.isRequired, 12 | navigation: PropTypes.object.isRequired, 13 | isWxArticle: PropTypes.bool, 14 | }; 15 | const defaultProps = { 16 | articleTabs: [], 17 | isWxArticle: false, 18 | }; 19 | 20 | class ArticleTabComponent extends PureComponent { 21 | constructor(props) { 22 | super(props); 23 | this.state = {}; 24 | } 25 | createTabs() { 26 | const {articleTabs, navigation, isWxArticle} = this.props; 27 | let routeConfigMap = {}; 28 | articleTabs.map( 29 | el => 30 | (routeConfigMap[el.name] = { 31 | screen: () => ( 32 | 37 | ), 38 | }), 39 | ); 40 | return routeConfigMap; 41 | } 42 | 43 | render() { 44 | const {articleTabs, themeColor} = this.props; 45 | if (!articleTabs.length) { 46 | return null; 47 | } 48 | const TabComponent = createAppContainer( 49 | createMaterialTopTabNavigator(this.createTabs(), { 50 | lazy: true, 51 | swipeEnabled: true, 52 | animationEnabled: true, 53 | backBehavior: 'none', 54 | tabBarOptions: { 55 | activeTintColor: Color.WHITE, 56 | inactiveTintColor: Color.TEXT_TAB_INACTIVE, 57 | scrollEnabled: true, 58 | tabStyle: { 59 | height: dp(70), 60 | width: dp(270), 61 | }, 62 | labelStyle: { 63 | fontSize: dp(28), 64 | paddingBottom: dp(25), 65 | fontWeight: 'bold', 66 | }, 67 | indicatorStyle: { 68 | height: dp(4), 69 | backgroundColor: Color.WHITE, 70 | width: dp(100), 71 | marginLeft: dp(85), 72 | }, // 下划线样式 73 | style: { 74 | backgroundColor: themeColor, 75 | height: dp(80), 76 | }, 77 | }, 78 | }), 79 | ); 80 | return ; 81 | } 82 | } 83 | 84 | ArticleTabComponent.propTypes = propTypes; 85 | ArticleTabComponent.defaultProps = defaultProps; 86 | 87 | const mapStateToProps = state => { 88 | return { 89 | themeColor: state.user.themeColor, 90 | }; 91 | }; 92 | 93 | export default connect(mapStateToProps)(ArticleTabComponent); 94 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import httpUtil from '../utils/httpUtil'; 5 | 6 | // 首页轮播 7 | export async function getHomeBanner() { 8 | return await httpUtil.get('banner/json'); 9 | } 10 | 11 | // 首页文章列表 12 | export function getHomeList(page = 0) { 13 | return httpUtil.get(`article/list/${page}/json`); 14 | } 15 | 16 | // 首页文章收藏 id:文章id 17 | export function addCollectArticle(id) { 18 | return httpUtil.post(`lg/collect/${id}/json`, {id}); 19 | } 20 | 21 | // 首页文章取消收藏 id:文章id 22 | export function cancelCollectArticle(id) { 23 | return httpUtil.post(`lg/uncollect_originId/${id}/json`, {id}); 24 | } 25 | 26 | /** 27 | * 我的收藏页-取消收藏 28 | * @param id:拼接在链接上 29 | * @param originId:列表页下发,无则为-1 30 | */ 31 | export function cancelMyCollectArticle(id, originId = -1) { 32 | return httpUtil.post(`lg/uncollect/${id}/json`, {id, originId}); 33 | } 34 | 35 | /** 36 | * 注册 37 | * @params username,password,repassword 38 | */ 39 | export function goToRegister(params) { 40 | return httpUtil.post('user/register', params); 41 | } 42 | 43 | /** 44 | * 登录 45 | * @params username,password 46 | */ 47 | export function goToLogin(params) { 48 | return httpUtil.post('user/login', params); 49 | } 50 | 51 | // 退出登录 52 | export function goToLogout() { 53 | return httpUtil.get('user/logout/json'); 54 | } 55 | 56 | // 体系 57 | export function getSystemData() { 58 | return httpUtil.get('tree/json'); 59 | } 60 | 61 | // 获取公众号Tabs列表 62 | export function getWxArticleTabs() { 63 | return httpUtil.get('wxarticle/chapters/json'); 64 | } 65 | 66 | /** 67 | * 查看某个公众号历史数据 68 | * @param id 公众号ID 69 | * @param page 公众号页码 70 | */ 71 | export function getWxArticleList(id, page) { 72 | return httpUtil.get(`wxarticle/list/${id}/${page}/json`); 73 | } 74 | 75 | // 获取导航数据 76 | export function getGuideData() { 77 | return httpUtil.get('navi/json'); 78 | } 79 | 80 | // 获取导航数据 81 | export function getProjectTree() { 82 | return httpUtil.get('project/tree/json'); 83 | } 84 | 85 | // 常用网站 86 | export function getOftenUsedWebsites() { 87 | return httpUtil.get('friend/json'); 88 | } 89 | 90 | // 搜索热词 91 | export function getSearchHotKey() { 92 | return httpUtil.get('hotkey/json'); 93 | } 94 | 95 | /** 96 | * 根据关键词搜索文章 97 | * @param k 关键词 98 | * @param page 文章页码 99 | */ 100 | export function getSearchArticle(k, page = 0) { 101 | return httpUtil.post(`article/query/${page}/json`, {k}); 102 | } 103 | 104 | // 我收藏的文章列表 105 | export function getCollectArticleList(page = 0) { 106 | return httpUtil.get(`lg/collect/list/${page}/json`); 107 | } 108 | 109 | // 获取个人积分获取列表,需要登录后访问 110 | export function getMyCoinList(page = 1) { 111 | return httpUtil.get(`lg/coin/list/${page}/json`); 112 | } 113 | 114 | export function getMyCoinInfo() { 115 | return httpUtil.get('lg/coin/userinfo/json'); 116 | } 117 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' 3 | 4 | target 'RN_WanAndroid' do 5 | # Pods for RN_WanAndroid 6 | pod 'FBLazyVector', :path => "../node_modules/react-native/Libraries/FBLazyVector" 7 | pod 'FBReactNativeSpec', :path => "../node_modules/react-native/Libraries/FBReactNativeSpec" 8 | pod 'RCTRequired', :path => "../node_modules/react-native/Libraries/RCTRequired" 9 | pod 'RCTTypeSafety', :path => "../node_modules/react-native/Libraries/TypeSafety" 10 | pod 'React', :path => '../node_modules/react-native/' 11 | pod 'React-Core', :path => '../node_modules/react-native/' 12 | pod 'React-CoreModules', :path => '../node_modules/react-native/React/CoreModules' 13 | pod 'React-Core/DevSupport', :path => '../node_modules/react-native/' 14 | pod 'React-RCTActionSheet', :path => '../node_modules/react-native/Libraries/ActionSheetIOS' 15 | pod 'React-RCTAnimation', :path => '../node_modules/react-native/Libraries/NativeAnimation' 16 | pod 'React-RCTBlob', :path => '../node_modules/react-native/Libraries/Blob' 17 | pod 'React-RCTImage', :path => '../node_modules/react-native/Libraries/Image' 18 | pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS' 19 | pod 'React-RCTNetwork', :path => '../node_modules/react-native/Libraries/Network' 20 | pod 'React-RCTSettings', :path => '../node_modules/react-native/Libraries/Settings' 21 | pod 'React-RCTText', :path => '../node_modules/react-native/Libraries/Text' 22 | pod 'React-RCTVibration', :path => '../node_modules/react-native/Libraries/Vibration' 23 | pod 'React-Core/RCTWebSocket', :path => '../node_modules/react-native/' 24 | 25 | pod 'React-cxxreact', :path => '../node_modules/react-native/ReactCommon/cxxreact' 26 | pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi' 27 | pod 'React-jsiexecutor', :path => '../node_modules/react-native/ReactCommon/jsiexecutor' 28 | pod 'React-jsinspector', :path => '../node_modules/react-native/ReactCommon/jsinspector' 29 | pod 'ReactCommon/jscallinvoker', :path => "../node_modules/react-native/ReactCommon" 30 | pod 'ReactCommon/turbomodule/core', :path => "../node_modules/react-native/ReactCommon" 31 | pod 'Yoga', :path => '../node_modules/react-native/ReactCommon/yoga' 32 | 33 | pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec' 34 | pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec' 35 | pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec' 36 | 37 | pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons' 38 | 39 | target 'RN_WanAndroidTests' do 40 | inherit! :search_paths 41 | # Pods for testing 42 | end 43 | 44 | use_native_modules! 45 | end 46 | 47 | target 'RN_WanAndroid-tvOS' do 48 | # Pods for RN_WanAndroid-tvOS 49 | 50 | target 'RN_WanAndroid-tvOSTests' do 51 | inherit! :search_paths 52 | # Pods for testing 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "12.0", 8 | "subtype" : "2688h", 9 | "scale" : "3x" 10 | }, 11 | { 12 | "orientation" : "landscape", 13 | "idiom" : "iphone", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "12.0", 16 | "subtype" : "2688h", 17 | "scale" : "3x" 18 | }, 19 | { 20 | "orientation" : "portrait", 21 | "idiom" : "iphone", 22 | "extent" : "full-screen", 23 | "minimum-system-version" : "12.0", 24 | "subtype" : "1792h", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "orientation" : "landscape", 29 | "idiom" : "iphone", 30 | "extent" : "full-screen", 31 | "minimum-system-version" : "12.0", 32 | "subtype" : "1792h", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "extent" : "full-screen", 37 | "idiom" : "iphone", 38 | "subtype" : "2436h", 39 | "filename" : "splash_bg.png", 40 | "minimum-system-version" : "11.0", 41 | "orientation" : "portrait", 42 | "scale" : "3x" 43 | }, 44 | { 45 | "orientation" : "landscape", 46 | "idiom" : "iphone", 47 | "extent" : "full-screen", 48 | "minimum-system-version" : "11.0", 49 | "subtype" : "2436h", 50 | "scale" : "3x" 51 | }, 52 | { 53 | "orientation" : "portrait", 54 | "idiom" : "iphone", 55 | "extent" : "full-screen", 56 | "minimum-system-version" : "8.0", 57 | "subtype" : "736h", 58 | "scale" : "3x" 59 | }, 60 | { 61 | "orientation" : "landscape", 62 | "idiom" : "iphone", 63 | "extent" : "full-screen", 64 | "minimum-system-version" : "8.0", 65 | "subtype" : "736h", 66 | "scale" : "3x" 67 | }, 68 | { 69 | "orientation" : "portrait", 70 | "idiom" : "iphone", 71 | "extent" : "full-screen", 72 | "minimum-system-version" : "8.0", 73 | "subtype" : "667h", 74 | "scale" : "2x" 75 | }, 76 | { 77 | "orientation" : "portrait", 78 | "idiom" : "iphone", 79 | "extent" : "full-screen", 80 | "minimum-system-version" : "7.0", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "orientation" : "portrait", 85 | "idiom" : "iphone", 86 | "extent" : "full-screen", 87 | "minimum-system-version" : "7.0", 88 | "subtype" : "retina4", 89 | "scale" : "2x" 90 | }, 91 | { 92 | "orientation" : "portrait", 93 | "idiom" : "iphone", 94 | "extent" : "full-screen", 95 | "scale" : "1x" 96 | }, 97 | { 98 | "orientation" : "portrait", 99 | "idiom" : "iphone", 100 | "extent" : "full-screen", 101 | "scale" : "2x" 102 | }, 103 | { 104 | "orientation" : "portrait", 105 | "idiom" : "iphone", 106 | "extent" : "full-screen", 107 | "subtype" : "retina4", 108 | "scale" : "2x" 109 | } 110 | ], 111 | "info" : { 112 | "version" : 1, 113 | "author" : "xcode" 114 | } 115 | } -------------------------------------------------------------------------------- /src/component/BottomTabBar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-02 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import { 6 | StyleSheet, 7 | Text, 8 | TouchableOpacity, 9 | View, 10 | TouchableNativeFeedback, 11 | } from 'react-native'; 12 | import {connect} from 'react-redux'; 13 | import Icon from 'react-native-vector-icons/Ionicons'; 14 | import {DEVICE_WIDTH, getRealDP as dp, isAndroid} from '../utils/screenUtil'; 15 | import Color from '../utils/Color'; 16 | import {i18n} from '../utils/Utility'; 17 | 18 | class BottomTabBar extends PureComponent { 19 | render() { 20 | const {route, focused, themeColor} = this.props; 21 | const {routeName} = route; 22 | let tabBarLabel, tabBarIconName, tabBarIconSize; 23 | switch (routeName) { 24 | case 'Home': 25 | tabBarLabel = i18n('home'); 26 | tabBarIconName = 'ios-home'; 27 | tabBarIconSize = dp(50); 28 | break; 29 | case 'System': 30 | tabBarLabel = i18n('system'); 31 | tabBarIconName = 'ios-school'; 32 | tabBarIconSize = dp(55); 33 | break; 34 | case 'WxArticle': 35 | tabBarLabel = i18n('publicAccount'); 36 | tabBarIconName = 'ios-people'; 37 | tabBarIconSize = dp(64); 38 | break; 39 | case 'Guide': 40 | tabBarLabel = i18n('Navigation'); 41 | tabBarIconName = 'ios-rocket'; 42 | tabBarIconSize = dp(50); 43 | break; 44 | default: 45 | tabBarLabel = i18n('project'); 46 | tabBarIconName = 'ios-paper'; 47 | tabBarIconSize = dp(50); 48 | break; 49 | } 50 | const tabBarColor = focused ? themeColor : Color.TEXT_LIGHT; 51 | const content = ( 52 | 53 | 54 | 59 | 60 | 61 | {tabBarLabel} 62 | 63 | 64 | ); 65 | if (isAndroid) { 66 | return ( 67 | 73 | {content} 74 | 75 | ); 76 | } 77 | return ( 78 | 79 | {content} 80 | 81 | ); 82 | } 83 | } 84 | 85 | const styles = StyleSheet.create({ 86 | tabBarWrapper: { 87 | width: DEVICE_WIDTH / 5, 88 | height: dp(100), 89 | justifyContent: 'center', 90 | alignItems: 'center', 91 | }, 92 | tabBarLabel: { 93 | fontSize: dp(20), 94 | }, 95 | iconWrapper: { 96 | width: dp(65), 97 | height: dp(65), 98 | justifyContent: 'center', 99 | alignItems: 'center', 100 | }, 101 | }); 102 | 103 | const mapStateToProps = state => { 104 | return { 105 | themeColor: state.user.themeColor, 106 | language: state.user.language, 107 | }; 108 | }; 109 | 110 | export default connect(mapStateToProps)(BottomTabBar); 111 | -------------------------------------------------------------------------------- /src/utils/Utility.js: -------------------------------------------------------------------------------- 1 | import {Platform, ToastAndroid} from 'react-native'; 2 | import {getRealDP as dp, getStatusBarHeight, isAndroid} from './screenUtil'; 3 | import Color from './Color'; 4 | import LanguageUtil from './LanguageUtil'; 5 | 6 | /** 7 | * 吐司方法 8 | * @param info 吐司文案信息 9 | * @return 10 | */ 11 | export function showToast(info) { 12 | if (isAndroid) { 13 | return ToastAndroid.show(info, ToastAndroid.SHORT); 14 | } 15 | if (!global.toast) { 16 | return; 17 | } 18 | global.toast.show(info); 19 | } 20 | 21 | /** 22 | * 获取默认"react-navigation"导航样式 23 | * @param navigation 24 | * @param backgroundColor 25 | * @return 导航样式 26 | */ 27 | export function getNavBarStyles(navigation, backgroundColor = Color.THEME) { 28 | return { 29 | title: navigation.getParam('title', ''), 30 | headerStyle: Platform.select({ 31 | ios: { 32 | backgroundColor: backgroundColor, 33 | height: dp(110), 34 | }, 35 | android: { 36 | backgroundColor: backgroundColor, 37 | height: dp(110) + getStatusBarHeight(), 38 | paddingTop: getStatusBarHeight(), 39 | }, 40 | }), 41 | }; 42 | } 43 | 44 | /** 45 | * 获取首页专题背景色 46 | * @param index 专题id 47 | * @return 背景色 48 | */ 49 | export function getChapterBgColor(index) { 50 | const indexStr = index.toString(); 51 | const id = indexStr.substr(indexStr.length - 1); 52 | const colors = [ 53 | '#79CDCD', 54 | '#71C671', 55 | '#4169E1', 56 | '#3CB371', 57 | '#EE82EE', 58 | '#F4A460', 59 | '#FF7256', 60 | '#5F9EA0', 61 | '#79CDCD', 62 | '#BC8F8F', 63 | ]; 64 | return colors[id]; 65 | } 66 | 67 | /** 68 | * APP主题色数据 69 | */ 70 | export function getThemeColorDataSource() { 71 | return [ 72 | {name: '默认主题色', color: '#2196F3'}, 73 | {name: '皇家蓝', color: '#4169E1'}, 74 | {name: '军校蓝', color: '#5F9EA0'}, 75 | {name: '深卡其布', color: '#BDB76B'}, 76 | {name: '玫瑰棕色', color: '#BC8F8F'}, 77 | {name: '印度红', color: '#CD5C5C'}, 78 | {name: '深石板灰', color: '#2F4F4F'}, 79 | {name: '海洋绿', color: '#2E8B57'}, 80 | {name: '暗淡的灰色', color: '#696969'}, 81 | {name: '橙色', color: '#FFA500'}, 82 | {name: '粉红色', color: '#FFC0CB'}, 83 | {name: '深粉色', color: '#FF1493'}, 84 | {name: '兰花的紫色', color: '#DA70D6'}, 85 | {name: '适中的紫色', color: '#9370DB'}, 86 | {name: '紫色', color: '#800080'}, 87 | {name: '纯黑', color: '#000000'}, 88 | ]; 89 | } 90 | 91 | export function i18n(Text) { 92 | return LanguageUtil.t(Text); 93 | } 94 | 95 | /** 96 | * 抽屉列表数据 97 | */ 98 | export function getDrawerData() { 99 | return [ 100 | {iconName: 'md-trending-up', title: i18n('my-points')}, 101 | {iconName: 'md-heart', title: i18n('my-collection')}, 102 | {iconName: 'md-globe', title: i18n('frequently-used-websites')}, 103 | {iconName: 'md-person', title: i18n('about-author')}, 104 | {iconName: 'md-share', title: i18n('share-app')}, 105 | {iconName: 'md-settings', title: i18n('settings')}, 106 | {iconName: 'md-power', title: i18n('logout')}, 107 | ]; 108 | } 109 | 110 | export function getLanguageList() { 111 | return [ 112 | {language: '简体中文', languageCode: 'zhHans'}, 113 | {language: '繁体中文', languageCode: 'zhHant'}, 114 | {language: 'English', languageCode: 'en'}, 115 | ]; 116 | } 117 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/utils/storageUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-19 3 | */ 4 | import {merge} from 'lodash'; 5 | import AsyncStorage from '@react-native-community/async-storage'; 6 | 7 | const storageUtil = { 8 | /** 9 | * Get a one or more value for a key or array of keys from AsyncStorage 10 | * @param {String|Array} key A key or array of keys 11 | * @return {Promise} 12 | */ 13 | get(key) { 14 | if (!Array.isArray(key)) { 15 | return AsyncStorage.getItem(key).then(value => { 16 | return JSON.parse(value); 17 | }); 18 | } else { 19 | return AsyncStorage.multiGet(key).then(values => { 20 | return values.map(value => { 21 | return JSON.parse(value[1]); 22 | }); 23 | }); 24 | } 25 | }, 26 | 27 | /** 28 | * Save a key value pair or a series of key value pairs to AsyncStorage. 29 | * @param {String|Array} key The key or an array of key/value pairs 30 | * @param {Any} value The value to save 31 | * @return {Promise} 32 | */ 33 | save(key, value) { 34 | if (!Array.isArray(key)) { 35 | return AsyncStorage.setItem(key, JSON.stringify(value)); 36 | } else { 37 | const pairs = key.map(function(pair) { 38 | return [pair[0], JSON.stringify(pair[1])]; 39 | }); 40 | return AsyncStorage.multiSet(pairs); 41 | } 42 | }, 43 | 44 | /** 45 | * Updates the value in the store for a given key in AsyncStorage. If the value is a string it will be replaced. If the value is an object it will be deep merged. 46 | * @param {String} key The key 47 | * @param {Value} value The value to update with 48 | * @return {Promise} 49 | */ 50 | update(key, value) { 51 | return storageUtil.get(key).then(item => { 52 | value = typeof value === 'string' ? value : merge({}, item, value); 53 | return AsyncStorage.setItem(key, JSON.stringify(value)); 54 | }); 55 | }, 56 | 57 | /** 58 | * Delete the value for a given key in AsyncStorage. 59 | * @param {String|Array} key The key or an array of keys to be deleted 60 | * @return {Promise} 61 | */ 62 | delete(key) { 63 | if (Array.isArray(key)) { 64 | return AsyncStorage.multiRemove(key); 65 | } else { 66 | return AsyncStorage.removeItem(key); 67 | } 68 | }, 69 | 70 | /** 71 | * Get all keys in AsyncStorage. 72 | * @return {Promise} A promise which when it resolves gets passed the saved keys in AsyncStorage. 73 | */ 74 | keys() { 75 | return AsyncStorage.getAllKeys(); 76 | }, 77 | 78 | /** 79 | * Push a value onto an array stored in AsyncStorage by key or create a new array in AsyncStorage for a key if it's not yet defined. 80 | * @param {String} key They key 81 | * @param {Any} value The value to push onto the array 82 | * @return {Promise} 83 | */ 84 | push(key, value) { 85 | return storageUtil.get(key).then(currentValue => { 86 | if (currentValue === null) { 87 | // if there is no current value populate it with the new value 88 | return storageUtil.save(key, [value]); 89 | } 90 | if (Array.isArray(currentValue)) { 91 | return storageUtil.save(key, [...currentValue, value]); 92 | } 93 | throw new Error( 94 | `Existing value for key "${key}" must be of type null or Array, received ${typeof currentValue}.`, 95 | ); 96 | }); 97 | }, 98 | }; 99 | 100 | export default storageUtil; 101 | -------------------------------------------------------------------------------- /src/service/setAxios.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import axios from 'axios'; 5 | import {get} from 'lodash'; 6 | import qs from 'querystring'; 7 | import AuthUtil from '../utils/AuthUtil'; 8 | const BASE_URL = 'https://www.wanandroid.com/'; 9 | const headers = { 10 | Accept: 'application/json;charset=utf-8', 11 | 'Content-Type': 'multipart/form-data;charset=utf-8', 12 | }; 13 | 14 | /** 15 | * 打印请求响应时的关键日志信息 16 | * @param response 接口响应信息 17 | */ 18 | async function handleShowResponseLog(response) { 19 | const {config, data, request} = response; 20 | console.log('response', response); 21 | const cookie = get(response, 'headers.set-cookie[0]', ''); 22 | if (cookie) { 23 | await AuthUtil.saveCookie(cookie); 24 | } 25 | console.log('请求的url: ', `${config.method}:${request.responseURL}`); 26 | if (config.method === 'post') { 27 | console.log('请求的body: ', qs.parse(config.data)); 28 | } 29 | console.log('返回值: ', data); 30 | } 31 | 32 | export function setAxios() { 33 | axios.defaults.headers = headers; 34 | axios.defaults.baseURL = BASE_URL; 35 | axios.defaults.timeout = 6000; // 请求超时时间 36 | axios.interceptors.request.use( 37 | async config => { 38 | const Cookie = await AuthUtil.getCookie(); 39 | if (config.method === 'post') { 40 | let data = new FormData(); 41 | for (const i in config.data) { 42 | data.append(i, config.data[i]); 43 | } 44 | config.data = data; 45 | } 46 | if (config.url !== 'user/login' && Cookie) { 47 | config.headers = {Cookie}; 48 | } 49 | return config; 50 | }, 51 | function(error) { 52 | return Promise.reject(error); 53 | }, 54 | ); 55 | axios.interceptors.response.use( 56 | async response => { 57 | const {data} = response; 58 | await handleShowResponseLog(response); 59 | if (data.errorCode === 0) { 60 | return Promise.resolve(data); 61 | } 62 | return Promise.reject(data.errorMsg || '请求失败'); 63 | }, 64 | err => { 65 | console.log('err', err); 66 | if (err && err.response) { 67 | switch (err.response.status) { 68 | case 400: 69 | err.message = '请求错误(400)'; 70 | break; 71 | case 401: 72 | err.message = '未授权,请重新登录(401)'; 73 | break; 74 | case 403: 75 | err.message = '拒绝访问(403)'; 76 | break; 77 | case 404: 78 | err.message = '请求出错(404)'; 79 | break; 80 | case 408: 81 | err.message = '请求超时(408)'; 82 | break; 83 | case 500: 84 | err.message = '服务器错误(500)'; 85 | break; 86 | case 501: 87 | err.message = '服务未实现(501)'; 88 | break; 89 | case 502: 90 | err.message = '网络错误(502)'; 91 | break; 92 | case 503: 93 | err.message = '服务不可用(503)'; 94 | break; 95 | case 504: 96 | err.message = '网络超时(504)'; 97 | break; 98 | case 505: 99 | err.message = 'HTTP版本不受支持(505)'; 100 | break; 101 | default: 102 | err.message = `连接出错(${err.response.status})!`; 103 | } 104 | } else { 105 | err.message = '连接服务器失败!'; 106 | } 107 | return Promise.reject(err.message); 108 | }, 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/screen/drawer/WebsitesScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {StyleSheet, Text, View} from 'react-native'; 3 | import {connect} from 'react-redux'; 4 | import globalStyles from '../../styles/globalStyles'; 5 | import NavBar from '../../component/NavBar'; 6 | import {fetchOftenUsedWebsites} from '../../actions'; 7 | import {DEVICE_WIDTH, getRealDP as dp} from '../../utils/screenUtil'; 8 | import Color from '../../utils/Color'; 9 | import {getChapterBgColor, i18n} from '../../utils/Utility'; 10 | import Touchable from '../../component/Touchable'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import CommonFlatList from '../../component/CommonFlatList'; 13 | 14 | /** 15 | * 常用网站 16 | */ 17 | class WebsitesScreen extends PureComponent { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | isRefreshing: false, 22 | }; 23 | this.renderItem = this.renderItem.bind(this); 24 | this.onRefresh = this.onRefresh.bind(this); 25 | } 26 | 27 | componentDidMount() { 28 | fetchOftenUsedWebsites(); 29 | } 30 | 31 | async onRefresh() { 32 | this.setState({isRefreshing: true}); 33 | await fetchOftenUsedWebsites(); 34 | this.setState({isRefreshing: false}); 35 | } 36 | 37 | renderItem({item}) { 38 | const {navigation} = this.props; 39 | return ( 40 | 41 | 47 | navigation.navigate('WebView', {title: item.name, url: item.link}) 48 | }> 49 | {item.name} 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | renderSeparator = () => ; 57 | 58 | render() { 59 | const {navigation, websites} = this.props; 60 | return ( 61 | 62 | 66 | item.id.toString()} 69 | renderItem={this.renderItem} 70 | ItemSeparatorComponent={this.renderSeparator} 71 | ListHeaderComponent={this.renderSeparator} 72 | ListFooterComponent={this.renderSeparator} 73 | isRefreshing={this.state.isRefreshing} 74 | toRefresh={this.onRefresh} 75 | /> 76 | 77 | ); 78 | } 79 | } 80 | 81 | const styles = StyleSheet.create({ 82 | itemContainer: { 83 | alignItems: 'center', 84 | }, 85 | itemWrapper: { 86 | width: DEVICE_WIDTH * 0.9, 87 | flexDirection: 'row', 88 | backgroundColor: Color.WHITE, 89 | borderRadius: dp(60), 90 | paddingVertical: dp(40), 91 | paddingHorizontal: dp(50), 92 | justifyContent: 'space-between', 93 | }, 94 | nameText: { 95 | fontSize: dp(32), 96 | color: Color.WHITE, 97 | fontWeight: 'bold', 98 | }, 99 | }); 100 | 101 | const mapStateToProps = state => { 102 | return { 103 | websites: state.home.websites, 104 | themeColor: state.user.themeColor, 105 | }; 106 | }; 107 | 108 | export default connect(mapStateToProps)(WebsitesScreen); 109 | -------------------------------------------------------------------------------- /src/screen/drawer/CollectScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-29 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View} from 'react-native'; 6 | import globalStyles from '../../styles/globalStyles'; 7 | import NavBar from '../../component/NavBar'; 8 | import { 9 | fetchCollectArticleList, 10 | fetchCollectArticleMore, 11 | fetchMyCollectAddCollect, 12 | fetchMyCollectCancelCollect, 13 | } from '../../actions'; 14 | import {getRealDP as dp} from '../../utils/screenUtil'; 15 | import {connect} from 'react-redux'; 16 | import ArticleItemRow from '../../component/ArticleItemRow'; 17 | import ListFooter from '../../component/ListFooter'; 18 | import CommonFlatList from '../../component/CommonFlatList'; 19 | import {i18n} from '../../utils/Utility'; 20 | 21 | class CollectScreen extends PureComponent { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | isRefreshing: false, 26 | }; 27 | this.renderItem = this.renderItem.bind(this); 28 | this.renderFooter = this.renderFooter.bind(this); 29 | this.onRefresh = this.onRefresh.bind(this); 30 | this.onEndReached = this.onEndReached.bind(this); 31 | } 32 | 33 | componentDidMount() { 34 | fetchCollectArticleList(); 35 | } 36 | 37 | async onRefresh() { 38 | this.setState({isRefreshing: true}); 39 | await fetchCollectArticleList(this.state.key); 40 | this.setState({isRefreshing: false}); 41 | } 42 | 43 | onEndReached() { 44 | const {isFullData} = this.props; 45 | if (isFullData) { 46 | return; 47 | } 48 | fetchCollectArticleMore(this.props.page); 49 | } 50 | 51 | renderItem({item, index}) { 52 | const {navigation} = this.props; 53 | return ( 54 | { 58 | if (item.collect) { 59 | fetchMyCollectCancelCollect(item.id, item.originId, index); 60 | } else { 61 | fetchMyCollectAddCollect(item.originId, index); 62 | } 63 | }} 64 | /> 65 | ); 66 | } 67 | 68 | renderFooter() { 69 | const {isRenderFooter, isFullData, themeColor} = this.props; 70 | return ( 71 | 76 | ); 77 | } 78 | 79 | render() { 80 | const {navigation, dataSource} = this.props; 81 | return ( 82 | 83 | 84 | item.id.toString()} 87 | renderItem={this.renderItem} 88 | ListHeaderComponent={() => } 89 | ListFooterComponent={this.renderFooter} 90 | onEndReached={this.onEndReached} 91 | isRefreshing={this.state.isRefreshing} 92 | toRefresh={this.onRefresh} 93 | /> 94 | 95 | ); 96 | } 97 | } 98 | 99 | const mapStateToProps = state => { 100 | return { 101 | page: state.collect.page, 102 | dataSource: state.collect.dataSource, 103 | isRenderFooter: state.collect.isRenderFooter, 104 | isFullData: state.collect.isFullData, 105 | themeColor: state.user.themeColor, 106 | }; 107 | }; 108 | 109 | export default connect(mapStateToProps)(CollectScreen); 110 | -------------------------------------------------------------------------------- /src/component/CommonFlatList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-04 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import { 6 | View, 7 | FlatList, 8 | RefreshControl, 9 | StyleSheet, 10 | TouchableWithoutFeedback, 11 | } from 'react-native'; 12 | import PropTypes from 'prop-types'; 13 | import Color from '../utils/Color'; 14 | import {connect} from 'react-redux'; 15 | import {DEVICE_HEIGHT, getRealDP as dp} from '../utils/screenUtil'; 16 | import Icon from 'react-native-vector-icons/Ionicons'; 17 | import {i18n} from '../utils/Utility'; 18 | 19 | const propTypes = { 20 | toRefresh: PropTypes.func, 21 | }; 22 | 23 | const defaultProps = { 24 | toRefresh: () => {}, 25 | }; 26 | 27 | /** 28 | * FlatList通用组件 29 | */ 30 | class CommonFlatList extends PureComponent { 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | isShowTop: false, // 显示置顶按钮 35 | }; 36 | this.handleScrollToTop = this.handleScrollToTop.bind(this); 37 | this._onScroll = this._onScroll.bind(this); 38 | } 39 | 40 | handleScrollToTop() { 41 | this.flatListRef && 42 | this.flatListRef.scrollToOffset({animated: true, offset: 0}); 43 | } 44 | 45 | _onScroll(e) { 46 | const scrollY = e.nativeEvent.contentOffset.y; 47 | if (scrollY > DEVICE_HEIGHT) { 48 | this.setState({isShowTop: true}); 49 | } else { 50 | this.setState({isShowTop: false}); 51 | } 52 | } 53 | 54 | render() { 55 | const {isRefreshing, toRefresh, themeColor} = this.props; 56 | return ( 57 | 58 | { 60 | this.flatListRef = comp; 61 | }} 62 | onScroll={this._onScroll} 63 | onEndReachedThreshold={0.2} 64 | refreshControl={ 65 | 73 | } 74 | {...this.props} 75 | /> 76 | {this.state.isShowTop ? ( 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ) : null} 85 | 86 | ); 87 | } 88 | } 89 | 90 | const styles = StyleSheet.create({ 91 | container: { 92 | flex: 1, 93 | }, 94 | fixAndroidStyle: { 95 | position: 'absolute', 96 | bottom: dp(80), 97 | right: dp(45), 98 | width: dp(120), 99 | height: dp(120), 100 | backgroundColor: 'rgba(0,0,0,0.005)', // android View设置borderRadius需要外加一层带背景色的View 101 | }, 102 | topStyle: { 103 | width: dp(120), 104 | height: dp(120), 105 | borderRadius: dp(60), 106 | justifyContent: 'center', 107 | alignItems: 'center', 108 | opacity: 0.8, 109 | }, 110 | }); 111 | 112 | CommonFlatList.propTypes = propTypes; 113 | CommonFlatList.defaultProps = defaultProps; 114 | 115 | const mapStateToProps = state => { 116 | return { 117 | themeColor: state.user.themeColor, 118 | }; 119 | }; 120 | 121 | export default connect(mapStateToProps)(CommonFlatList); 122 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createAppContainer} from 'react-navigation'; 3 | import {createStackNavigator} from 'react-navigation-stack'; 4 | import {createDrawerNavigator} from 'react-navigation-drawer'; 5 | import {createBottomTabNavigator} from 'react-navigation-tabs'; 6 | import {getRealDP as dp} from './utils/screenUtil'; 7 | // import StackViewStyleInterpolator from 'react-navigation-stack/lib/commonjs/views/StackView/StackViewStyleInterpolator'; 8 | import BottomTabBar from './component/BottomTabBar'; 9 | // Screen 10 | import HomeScreen from './screen/tabs/HomeScreen'; 11 | import SystemScreen from './screen/tabs/SystemScreen'; 12 | import WxArticleScreen from './screen/tabs/WxArticleScreen'; 13 | import GuideScreen from './screen/tabs/GuideScreen'; 14 | import ProjectScreen from './screen/tabs/ProjectScreen'; 15 | import DrawerScreen from './screen/drawer/DrawerScreen'; 16 | import WebViewScreen from './screen/article/WebViewScreen'; 17 | import LoginScreen from './screen/drawer/LoginScreen'; 18 | import RegisterScreen from './screen/drawer/RegisterScreen'; 19 | import ArticleTabScreen from './screen/article/ArticleTabScreen'; 20 | import WebsitesScreen from './screen/drawer/WebsitesScreen'; 21 | import SearchScreen from './screen/article/SearchScreen'; 22 | import SearchArticleScreen from './screen/article/SearchArticleScreen'; 23 | import AboutScreen from './screen/drawer/AboutScreen'; 24 | import CollectScreen from './screen/drawer/CollectScreen'; 25 | import CoinDetailScreen from './screen/drawer/CoinDetailScreen'; 26 | import SettingScreen from './screen/drawer/SettingScreen'; 27 | import LanguageScreen from './screen/drawer/LanguageScreen'; 28 | 29 | const TabScreens = createBottomTabNavigator( 30 | { 31 | Home: HomeScreen, 32 | System: SystemScreen, 33 | WxArticle: WxArticleScreen, 34 | Guide: GuideScreen, 35 | Project: ProjectScreen, 36 | }, 37 | { 38 | defaultNavigationOptions: { 39 | tabBarButtonComponent: props => , 40 | }, 41 | tabBarOptions: { 42 | showLabel: false, 43 | showIcon: false, 44 | style: { 45 | height: dp(100), 46 | borderTopWidth: 0, 47 | }, 48 | }, 49 | }, 50 | ); 51 | 52 | const drawerNavigator = createDrawerNavigator( 53 | { 54 | Tabs: TabScreens, 55 | }, 56 | { 57 | drawerWidth: dp(600), 58 | contentComponent: DrawerScreen, 59 | hideStatusBar: false, 60 | statusBarAnimation: 'fade', 61 | defaultNavigationOptions: { 62 | drawerLockMode: 'unlocked', 63 | }, 64 | }, 65 | ); 66 | 67 | const RootStack = createStackNavigator( 68 | { 69 | Home: drawerNavigator, 70 | WebView: WebViewScreen, 71 | Login: LoginScreen, 72 | Register: RegisterScreen, 73 | ArticleTab: ArticleTabScreen, 74 | Websites: WebsitesScreen, 75 | Search: SearchScreen, 76 | SearchArticle: SearchArticleScreen, 77 | About: AboutScreen, 78 | Collect: CollectScreen, 79 | CoinDetail: CoinDetailScreen, 80 | Setting: SettingScreen, 81 | Language: LanguageScreen, 82 | }, 83 | { 84 | initialRouteName: 'Home', 85 | mode: 'modal', 86 | // 用于设置安卓卡片式跳转 87 | // transitionConfig: () => ({ 88 | // screenInterpolator: StackViewStyleInterpolator.forHorizontal, 89 | // }), 90 | navigationOptions: () => ({ 91 | gesturesEnabled: true, 92 | }), 93 | // 默认导航头样式设置 94 | defaultNavigationOptions: { 95 | header: null, 96 | }, 97 | }, 98 | ); 99 | 100 | export default createAppContainer(RootStack); 101 | -------------------------------------------------------------------------------- /src/component/Banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-19 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View, Image, Text, StyleSheet} from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | import Swiper from 'react-native-swiper'; 8 | import Touchable from './Touchable'; 9 | import {DEVICE_WIDTH, getRealDP as dp} from '../utils/screenUtil'; 10 | import Color from '../utils/Color'; 11 | 12 | const propTypes = { 13 | bannerArr: PropTypes.array.isRequired, 14 | navigation: PropTypes.object.isRequired, 15 | }; 16 | 17 | const defaultProps = { 18 | bannerArr: [], 19 | }; 20 | 21 | /** 22 | * 首页轮播组件 23 | */ 24 | class Banner extends PureComponent { 25 | constructor(props) { 26 | super(props); 27 | this.state = { 28 | currentBannerIndex: 0, 29 | }; 30 | this.getCurrentImgIndex = this.getCurrentImgIndex.bind(this); 31 | this.toShowWebView = this.toShowWebView.bind(this); 32 | } 33 | 34 | getCurrentImgIndex(imageIndex) { 35 | this.setState({currentBannerIndex: imageIndex}); 36 | } 37 | 38 | toShowWebView(info) { 39 | const {navigation} = this.props; 40 | const {title, url} = info; 41 | navigation.navigate('WebView', { 42 | title, 43 | url, 44 | }); 45 | } 46 | 47 | render() { 48 | const {bannerArr} = this.props; 49 | if (!bannerArr.length) { 50 | return ; 51 | } 52 | return ( 53 | 54 | 62 | {bannerArr.map(el => ( 63 | this.toShowWebView(el)}> 67 | 68 | 69 | ))} 70 | 71 | 72 | 73 | {bannerArr[this.state.currentBannerIndex].title} 74 | 75 | 76 | {this.state.currentBannerIndex + 1}/{bannerArr.length} 77 | 78 | 79 | 80 | ); 81 | } 82 | } 83 | 84 | const imageHeight = dp(380); 85 | const styles = StyleSheet.create({ 86 | defaultBg: { 87 | height: imageHeight, 88 | backgroundColor: Color.DEFAULT_BG, 89 | }, 90 | bannerContainer: { 91 | height: imageHeight, 92 | backgroundColor: Color.DEFAULT_BG, 93 | }, 94 | imgCarousel: { 95 | height: imageHeight, 96 | }, 97 | imgBanner: { 98 | width: DEVICE_WIDTH, 99 | height: imageHeight, 100 | resizeMode: 'stretch', 101 | }, 102 | bannerHint: { 103 | flex: 1, 104 | width: DEVICE_WIDTH, 105 | flexDirection: 'row', 106 | justifyContent: 'space-between', 107 | alignItems: 'center', 108 | paddingHorizontal: dp(20), 109 | backgroundColor: 'rgba(0,0,0,0.3)', 110 | height: dp(50), 111 | bottom: 0, 112 | left: 0, 113 | position: 'absolute', 114 | }, 115 | bannerText: { 116 | color: Color.WHITE, 117 | fontSize: dp(28), 118 | }, 119 | }); 120 | 121 | Banner.propTypes = propTypes; 122 | Banner.defaultProps = defaultProps; 123 | 124 | export default Banner; 125 | -------------------------------------------------------------------------------- /src/component/Touchable.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import { 3 | TouchableHighlight, 4 | TouchableNativeFeedback, 5 | TouchableWithoutFeedback, 6 | TouchableOpacity, 7 | } from 'react-native'; 8 | import PropTypes from 'prop-types'; 9 | import {isAndroid} from '../utils/screenUtil'; 10 | 11 | const propTypes = { 12 | isHighlight: PropTypes.bool, // 用于使用TouchableHighlight组件 13 | isNativeFeedback: PropTypes.bool, // 仅用于安卓平台,设置原生触摸响应效果 14 | isWithoutFeedback: PropTypes.bool, // 用于使用TouchableWithoutFeedback组件, 不推荐使用 15 | children: PropTypes.node, 16 | activeOpacity: PropTypes.number, // 用于TouchableOpacity和TouchableHighlight组件,设置触摸响应时不透明度 17 | underlayColor: PropTypes.string, // 仅用于TouchableHighlight组件,设置触摸响应时底层颜色 18 | onPress: PropTypes.func.isRequired, // 当触摸操作结束时调用, 处理触摸响应事件 19 | isPreventDouble: PropTypes.bool, // 是否开启防重复点击(加减计数,筛选器,选择器,展开收起等情况需禁用防重,设置为false) 20 | }; 21 | 22 | const defaultProps = { 23 | isHighlight: false, 24 | isNativeFeedback: false, 25 | isWithoutFeedback: false, 26 | children: null, 27 | activeOpacity: 0.85, 28 | underlayColor: '#e6e6e6', 29 | onPress: () => {}, 30 | isPreventDouble: true, 31 | }; 32 | 33 | const preventDoublePress = { 34 | lastPressTime: 1, 35 | onPress(callback) { 36 | const curTime = new Date().getTime(); 37 | if (curTime - this.lastPressTime > 500) { 38 | this.lastPressTime = curTime; 39 | callback(); 40 | } 41 | }, 42 | }; 43 | 44 | /** 45 | * 组件名称:防重复点击触摸组件 46 | * 注意事项:默认使用TouchableOpacity组件效果 47 | * */ 48 | class Touchable extends PureComponent { 49 | constructor(props) { 50 | super(props); 51 | this.state = {}; 52 | this.onPreventDoublePress = this.onPreventDoublePress.bind(this); 53 | } 54 | 55 | /** 56 | * 防重复点击处理事件 57 | * */ 58 | onPreventDoublePress() { 59 | const {onPress, isPreventDouble} = this.props; 60 | if (!onPress) { 61 | return; 62 | } 63 | if (isPreventDouble) { 64 | preventDoublePress.onPress(onPress); 65 | } else { 66 | onPress(); 67 | } 68 | } 69 | 70 | render() { 71 | const { 72 | children, 73 | isHighlight, 74 | isNativeFeedback, 75 | isWithoutFeedback, 76 | activeOpacity, 77 | underlayColor, 78 | } = this.props; 79 | 80 | /** TouchableHighlight 触摸底色高亮响应效果 */ 81 | if (isHighlight) { 82 | return ( 83 | 87 | {children} 88 | 89 | ); 90 | } 91 | 92 | /** TouchableNativeFeedback Android原生触摸响应效果 */ 93 | if (isAndroid && isNativeFeedback) { 94 | return ( 95 | 100 | {children} 101 | 102 | ); 103 | } 104 | 105 | /** TouchableWithoutFeedback 无视觉反馈触摸响应效果, 建议少使用 */ 106 | if (isWithoutFeedback) { 107 | return ( 108 | 111 | {children} 112 | 113 | ); 114 | } 115 | 116 | /** TouchableOpacity Android/IOS双平台默认触摸响应效果 */ 117 | return ( 118 | 122 | {children} 123 | 124 | ); 125 | } 126 | } 127 | 128 | Touchable.propTypes = propTypes; 129 | Touchable.defaultProps = defaultProps; 130 | 131 | export default Touchable; 132 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/screen/article/SearchArticleScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {View} from 'react-native'; 3 | import {connect} from 'react-redux'; 4 | import globalStyles from '../../styles/globalStyles'; 5 | import NavBar from '../../component/NavBar'; 6 | import { 7 | clearSearchArticle, 8 | fetchSearchArticle, 9 | fetchSearchArticleAddCollect, 10 | fetchSearchArticleCancelCollect, 11 | fetchSearchArticleMore, 12 | } from '../../actions'; 13 | import ArticleItemRow from '../../component/ArticleItemRow'; 14 | import ListFooter from '../../component/ListFooter'; 15 | import {getRealDP as dp} from '../../utils/screenUtil'; 16 | import CommonFlatList from '../../component/CommonFlatList'; 17 | import {i18n, showToast} from '../../utils/Utility'; 18 | 19 | /** 20 | * 搜索文章结果页 21 | */ 22 | class SearchArticleScreen extends PureComponent { 23 | constructor(props) { 24 | super(props); 25 | const {navigation} = props; 26 | const key = navigation.getParam('key', ''); 27 | this.state = { 28 | key, 29 | isRefreshing: false, 30 | }; 31 | this.renderItem = this.renderItem.bind(this); 32 | this.renderFooter = this.renderFooter.bind(this); 33 | this.onRefresh = this.onRefresh.bind(this); 34 | this.onEndReached = this.onEndReached.bind(this); 35 | } 36 | 37 | componentDidMount() { 38 | fetchSearchArticle(this.state.key); 39 | } 40 | 41 | async onRefresh() { 42 | this.setState({isRefreshing: true}); 43 | await fetchSearchArticle(this.state.key); 44 | this.setState({isRefreshing: false}); 45 | } 46 | 47 | onEndReached() { 48 | const {isFullData} = this.props; 49 | if (isFullData) { 50 | return; 51 | } 52 | fetchSearchArticleMore(this.state.key, this.props.page); 53 | } 54 | 55 | renderItem({item, index}) { 56 | const {navigation, isLogin} = this.props; 57 | return ( 58 | { 62 | if (!isLogin) { 63 | showToast(i18n('please-login-first')); 64 | return navigation.navigate('Login'); 65 | } 66 | if (item.collect) { 67 | fetchSearchArticleCancelCollect(item.id, index); 68 | } else { 69 | fetchSearchArticleAddCollect(item.id, index); 70 | } 71 | }} 72 | /> 73 | ); 74 | } 75 | 76 | renderFooter() { 77 | const {isRenderFooter, isFullData, themeColor} = this.props; 78 | return ( 79 | 84 | ); 85 | } 86 | 87 | render() { 88 | const {navigation, dataSource} = this.props; 89 | return ( 90 | 91 | { 95 | clearSearchArticle(); 96 | navigation.goBack(); 97 | }} 98 | /> 99 | item.id.toString()} 102 | renderItem={this.renderItem} 103 | ListHeaderComponent={() => } 104 | ListFooterComponent={this.renderFooter} 105 | onEndReached={this.onEndReached} 106 | isRefreshing={this.state.isRefreshing} 107 | toRefresh={this.onRefresh} 108 | /> 109 | 110 | ); 111 | } 112 | } 113 | 114 | const mapStateToProps = state => { 115 | return { 116 | page: state.search.page, 117 | dataSource: state.search.dataSource, 118 | isRenderFooter: state.search.isRenderFooter, 119 | isFullData: state.search.isFullData, 120 | themeColor: state.user.themeColor, 121 | isLogin: state.user.isLogin, 122 | }; 123 | }; 124 | 125 | export default connect(mapStateToProps)(SearchArticleScreen); 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native 版 WanAndroid 客户端 2 | 3 | **Github 项目地址:[https://github.com/aijason/RN_WanAndroid](https://github.com/aijason/RN_WanAndroid)** 4 | 5 | **开源不易,如果喜欢的话希望给个 `Star` 或 `Fork` ^_^ ,谢谢~~** 6 | 7 | ## 前言 8 | 这几年主要从事 `React Native` 的开发工作,最早接触到ReactNative的版本是`0.28^`,到最近的`0.61.2^`版本,跨度长达三年之久,爬过无数的坑,但还是对RN的未来充满信心,这次也非常感谢`WanAndroid开放API`,让我工作之余,用最新的RN版本体验下开发的乐趣! 9 | 10 | ## 简介 11 | [RN_WanAndroid](https://github.com/aijason/RN_WanAndroid) 采用 `React Native` 框架编写,基于0.61.2^版本。结合 `Axios` + `React-Navigation` + `Redux` 等开源框架设计的项目,项目代码结构清晰并且有详细注释,如有任何疑问和建议请提 [Issues](https://github.com/aijason/RN_WanAndroid/issues) 或联系 qq: **977854695** ,**项目会持续迭代维护,也欢迎PR ^_^,努力打造一款优秀的React Native框架的 [WanAndroid](http://www.wanandroid.com/) 客户端**。 12 | 13 | ## API 14 | [**玩 Android 开放 API**](http://www.wanandroid.com/blog/show/2) 15 | 16 | ## 下载体验 17 | 18 | - 点击[![](https://img.shields.io/badge/Download-apk-green.svg)](https://github.com/aijason/RN_WanAndroid/releases/download/v1.0.2/RN_WanAndroid-release.apk) 下载 19 | 20 | ## IOS项目截图 21 | 22 | | ![](screenshot/iOS_01.png) | ![](screenshot/iOS_02.png) | ![](screenshot/iOS_03.png) | ![](screenshot/iOS_04.png) | 23 | | --- | --- | --- | --- | 24 | | ![](screenshot/iOS_05.png) | ![](screenshot/iOS_06.png) | ![](screenshot/iOS_07.png) | ![](screenshot/iOS_08.png) | 25 | | ![](screenshot/iOS_09.png) | ![](screenshot/iOS_10.png) | ![](screenshot/iOS_11.png) | ![](screenshot/iOS_12.png) | 26 | 27 | ## Android项目截图 28 | 29 | | ![](screenshot/android_01.png) | ![](screenshot/android_02.png) | ![](screenshot/android_03.png) | ![](screenshot/android_04.png) | 30 | | --- | --- | --- | --- | 31 | | ![](screenshot/android_05.png) | ![](screenshot/android_06.png) | ![](screenshot/android_07.png) | ![](screenshot/android_08.png) | 32 | | ![](screenshot/android_09.png) | ![](screenshot/android_10.png) | ![](screenshot/android_11.png) | ![](screenshot/android_12.png) | 33 | 34 | ## 主要功能 35 | 36 | - 首页、体系、公众号、导航、项目五大模块; 37 | - 登录注册功能; 38 | - 搜索功能:热门搜索、搜索历史文章; 39 | - 收藏功能:添加收藏、取消收藏; 40 | - 浏览文章、分享文章; 41 | - 查看常用网站; 42 | - 自定义切换主题颜色功能; 43 | - 多语言切换功能; 44 | - 我的积分明细; 45 | - 关于模块。 46 | 47 | ## 主要开源框架 48 | 49 | - [axios](https://github.com/axios/axios) 50 | - [redux](https://github.com/reduxjs/redux) 51 | - [react-native](https://github.com/facebook/react-native) 52 | - [react-redux](https://github.com/reduxjs/react-redux) 53 | - [redux-thunk](https://github.com/reduxjs/redux-thunk) 54 | - [react-native-gesture-handler](https://github.com/kmagiera/react-native-gesture-handler) 55 | - [react-native-reanimated](https://github.com/kmagiera/react-native-reanimated) 56 | - [react-native-splash-screen](https://github.com/crazycodeboy/react-native-splash-screen) 57 | - [react-native-swiper](https://github.com/leecade/react-native-swiper) 58 | - [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) 59 | - [react-native-webview](https://github.com/react-native-community/react-native-webview) 60 | - [react-navigation](https://github.com/react-navigation/react-navigation) 61 | - [react-navigation-drawer](https://github.com/react-navigation/drawer) 62 | - [react-navigation-stack](https://github.com/react-navigation/stack) 63 | - [react-navigation-tabs](https://github.com/react-navigation/tabs) 64 | - [react-native-i18n](https://github.com/aijason/react-native-i18n) 65 | - [@react-native-community/async-storage](https://github.com/react-native-community/async-storage) 66 | 67 | ## Thanks 68 | 69 | **感谢所有优秀的开源项目 ^_^** 。 70 | 71 | ## Statement 72 | **项目中的 API 均来自于 [www.wanandroid.com](http://www.wanandroid.com/) 网站,纯属学习交流使用,不得用于商业用途。** 73 | 74 | ## LICENSE 75 | 76 | ``` 77 | Copyright 2019 aijason 78 | 79 | Licensed under the Apache License, Version 2.0 (the "License"); 80 | you may not use this file except in compliance with the License. 81 | You may obtain a copy of the License at 82 | 83 | http://www.apache.org/licenses/LICENSE-2.0 84 | 85 | Unless required by applicable law or agreed to in writing, software 86 | distributed under the License is distributed on an "AS IS" BASIS, 87 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 88 | See the License for the specific language governing permissions and 89 | limitations under the License. 90 | ``` 91 | -------------------------------------------------------------------------------- /src/screen/article/SearchScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {View, TextInput, StyleSheet, Text, Keyboard} from 'react-native'; 3 | import {connect} from 'react-redux'; 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | import globalStyles from '../../styles/globalStyles'; 6 | import NavBar from '../../component/NavBar'; 7 | import {DEVICE_WIDTH, getRealDP as dp} from '../../utils/screenUtil'; 8 | import Color from '../../utils/Color'; 9 | import {fetchSearchHotKey} from '../../actions'; 10 | import {getChapterBgColor, i18n, showToast} from '../../utils/Utility'; 11 | import Touchable from '../../component/Touchable'; 12 | 13 | /** 14 | * SearchScreen 15 | */ 16 | class SearchScreen extends PureComponent { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | searchKey: '', 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | fetchSearchHotKey(); 26 | } 27 | 28 | render() { 29 | const {navigation, hotKey} = this.props; 30 | return ( 31 | 32 | ( 35 | 36 | { 43 | this.setState({searchKey}); 44 | }} 45 | /> 46 | 47 | )} 48 | rightIcon={'md-search'} 49 | onRightPress={() => { 50 | Keyboard.dismiss(); 51 | if (!this.state.searchKey) { 52 | return showToast(i18n('please-enter-search-keywords')); 53 | } 54 | navigation.navigate('SearchArticle', {key: this.state.searchKey}); 55 | }} 56 | /> 57 | 58 | 59 | {i18n('popular-search')} 60 | 61 | 62 | {hotKey.map(el => ( 63 | 66 | navigation.navigate('SearchArticle', {key: el.name}) 67 | } 68 | style={[ 69 | styles.hotKeyItem, 70 | {backgroundColor: getChapterBgColor(el.id)}, 71 | ]}> 72 | {el.name} 73 | 74 | ))} 75 | 76 | 77 | ); 78 | } 79 | } 80 | 81 | const styles = StyleSheet.create({ 82 | textInputWrapper: { 83 | width: DEVICE_WIDTH - dp(260), 84 | }, 85 | textInputStyle: { 86 | fontSize: dp(32), 87 | color: Color.WHITE, 88 | padding: 0, 89 | }, 90 | hotKeyTitleWrapper: { 91 | height: dp(100), 92 | paddingHorizontal: dp(28), 93 | alignItems: 'center', 94 | flexDirection: 'row', 95 | }, 96 | hotKeyTitleText: { 97 | fontSize: dp(38), 98 | color: Color.TEXT_MAIN, 99 | marginLeft: dp(15), 100 | }, 101 | hotKeyWrapper: { 102 | paddingHorizontal: dp(28), 103 | flexWrap: 'wrap', 104 | flexDirection: 'row', 105 | }, 106 | hotKeyItem: { 107 | paddingHorizontal: dp(30), 108 | paddingVertical: dp(15), 109 | borderRadius: dp(35), 110 | justifyContent: 'center', 111 | alignItems: 'center', 112 | marginRight: dp(20), 113 | marginBottom: dp(20), 114 | }, 115 | hotKeyText: { 116 | fontSize: dp(30), 117 | color: Color.WHITE, 118 | fontWeight: 'bold', 119 | }, 120 | }); 121 | 122 | const mapStateToProps = state => { 123 | return { 124 | hotKey: state.search.hotKey, 125 | themeColor: state.user.themeColor, 126 | }; 127 | }; 128 | 129 | export default connect(mapStateToProps)(SearchScreen); 130 | -------------------------------------------------------------------------------- /src/utils/Toast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by hjs on 2019-09-17 3 | */ 4 | import React, {Component} from 'react'; 5 | import { 6 | StyleSheet, 7 | View, 8 | Animated, 9 | Dimensions, 10 | Text, 11 | ViewPropTypes as RNViewPropTypes, 12 | } from 'react-native'; 13 | 14 | import PropTypes from 'prop-types'; 15 | const ViewPropTypes = RNViewPropTypes || View.propTypes; 16 | export const DURATION = { 17 | LENGTH_SHORT: 1500, 18 | FOREVER: 0, 19 | }; 20 | 21 | const {height, width} = Dimensions.get('window'); 22 | 23 | class Toast extends Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | isShow: false, 28 | text: '', 29 | opacityValue: new Animated.Value(this.props.opacity), 30 | }; 31 | } 32 | 33 | show(text, duration, callback) { 34 | this.duration = 35 | typeof duration === 'number' ? duration : DURATION.LENGTH_SHORT; 36 | this.callback = callback; 37 | this.setState({ 38 | isShow: true, 39 | text: text, 40 | }); 41 | 42 | this.animation = Animated.timing(this.state.opacityValue, { 43 | toValue: this.props.opacity, 44 | duration: this.props.fadeInDuration, 45 | }); 46 | this.animation.start(() => { 47 | this.isShow = true; 48 | if (duration !== DURATION.FOREVER) { 49 | this.close(); 50 | } 51 | }); 52 | } 53 | 54 | close(duration) { 55 | let delay = typeof duration === 'undefined' ? this.duration : duration; 56 | 57 | if (delay === DURATION.FOREVER) { 58 | delay = this.props.defaultCloseDelay || 250; 59 | } 60 | 61 | if (!this.isShow && !this.state.isShow) { 62 | return; 63 | } 64 | this.timer && clearTimeout(this.timer); 65 | this.timer = setTimeout(() => { 66 | this.animation = Animated.timing(this.state.opacityValue, { 67 | toValue: 0.0, 68 | duration: this.props.fadeOutDuration, 69 | }); 70 | this.animation.start(() => { 71 | this.setState({ 72 | isShow: false, 73 | }); 74 | this.isShow = false; 75 | if (typeof this.callback === 'function') { 76 | this.callback(); 77 | } 78 | }); 79 | }, delay); 80 | } 81 | 82 | componentWillUnmount() { 83 | this.animation && this.animation.stop(); 84 | this.timer && clearTimeout(this.timer); 85 | } 86 | 87 | render() { 88 | let pos; 89 | switch (this.props.position) { 90 | case 'top': 91 | pos = this.props.positionValue; 92 | break; 93 | case 'center': 94 | pos = height / 2; 95 | break; 96 | case 'bottom': 97 | pos = height - this.props.positionValue; 98 | break; 99 | } 100 | 101 | const view = this.state.isShow ? ( 102 | 103 | 109 | {React.isValidElement(this.state.text) ? ( 110 | this.state.text 111 | ) : ( 112 | {this.state.text} 113 | )} 114 | 115 | 116 | ) : null; 117 | return view; 118 | } 119 | } 120 | 121 | const styles = StyleSheet.create({ 122 | container: { 123 | position: 'absolute', 124 | left: 0, 125 | right: 0, 126 | elevation: 999, 127 | alignItems: 'center', 128 | zIndex: 10000, 129 | }, 130 | content: { 131 | backgroundColor: '#696969', 132 | borderRadius: 5, 133 | padding: 10, 134 | }, 135 | text: { 136 | color: 'white', 137 | }, 138 | }); 139 | 140 | Toast.propTypes = { 141 | style: ViewPropTypes.style, 142 | position: PropTypes.oneOf(['top', 'center', 'bottom']), 143 | textStyle: Text.propTypes.style, 144 | positionValue: PropTypes.number, 145 | fadeInDuration: PropTypes.number, 146 | fadeOutDuration: PropTypes.number, 147 | opacity: PropTypes.number, 148 | }; 149 | 150 | Toast.defaultProps = { 151 | position: 'bottom', 152 | textStyle: styles.text, 153 | positionValue: 200, 154 | fadeInDuration: 500, 155 | fadeOutDuration: 500, 156 | opacity: 0.9, 157 | }; 158 | 159 | export default Toast; 160 | -------------------------------------------------------------------------------- /src/screen/drawer/AboutScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-29 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {Image, StyleSheet, Text, View} from 'react-native'; 6 | import Color from '../../utils/Color'; 7 | import NavBar from '../../component/NavBar'; 8 | import globalStyles from '../../styles/globalStyles'; 9 | import { 10 | DEVICE_WIDTH, 11 | getBottomSpace, 12 | getRealDP as dp, 13 | } from '../../utils/screenUtil'; 14 | import images from '../../images'; 15 | import {connect} from 'react-redux'; 16 | import {i18n} from '../../utils/Utility'; 17 | 18 | /** 19 | * 关于作者 20 | */ 21 | class AboutScreen extends PureComponent { 22 | render() { 23 | const {navigation, themeColor} = this.props; 24 | const gitHubStr = 'https://github.com/aijason'; 25 | const csdnStr = 'https://blog.csdn.net/u010379595'; 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | {i18n('wanAndroid-client-based-on-Facebook-react-native')} 33 | 34 | 35 | 36 | {i18n('email')}: 37 | 977854695@qq.com 38 | 39 | 40 | 41 | 42 | CSDN: 43 | { 46 | navigation.navigate('WebView', { 47 | title: 'CSDN', 48 | url: csdnStr, 49 | }); 50 | }}> 51 | {csdnStr} 52 | 53 | 54 | 55 | 56 | 57 | GitHub: 58 | { 61 | navigation.navigate('WebView', { 62 | title: 'GitHub', 63 | url: gitHubStr, 64 | }); 65 | }}> 66 | {gitHubStr} 67 | 68 | 69 | 70 | 71 | {i18n( 72 | 'This project is for learning purposes only, not for commercial purposes', 73 | )} 74 | 75 | 76 | 77 | ); 78 | } 79 | } 80 | 81 | const styles = StyleSheet.create({ 82 | content: { 83 | flex: 1, 84 | backgroundColor: Color.WHITE, 85 | alignItems: 'center', 86 | paddingVertical: dp(150), 87 | }, 88 | logo: { 89 | width: dp(180), 90 | height: dp(189), 91 | borderRadius: dp(90), 92 | marginBottom: dp(50), 93 | }, 94 | desc: { 95 | fontSize: dp(30), 96 | color: Color.TEXT_MAIN, 97 | marginTop: dp(20), 98 | marginBottom: dp(100), 99 | }, 100 | blogStyle: { 101 | width: dp(200), 102 | height: dp(200), 103 | borderRadius: dp(100), 104 | backgroundColor: '#FF7256', 105 | justifyContent: 'center', 106 | alignItems: 'center', 107 | marginBottom: dp(20), 108 | marginHorizontal: dp(20), 109 | }, 110 | githubStyle: { 111 | width: dp(200), 112 | height: dp(200), 113 | borderRadius: dp(100), 114 | backgroundColor: '#5F9EA0', 115 | justifyContent: 'center', 116 | alignItems: 'center', 117 | marginBottom: dp(20), 118 | marginHorizontal: dp(20), 119 | }, 120 | item: { 121 | paddingHorizontal: dp(50), 122 | marginBottom: dp(50), 123 | width: DEVICE_WIDTH, 124 | }, 125 | itemText: { 126 | fontSize: dp(30), 127 | color: Color.TEXT_MAIN, 128 | }, 129 | bottomText: { 130 | fontSize: dp(24), 131 | color: Color.TEXT_MAIN, 132 | position: 'absolute', 133 | marginVertical: dp(50), 134 | paddingHorizontal: dp(28), 135 | bottom: getBottomSpace(), 136 | }, 137 | underlineText: { 138 | textDecorationLine: 'underline', 139 | }, 140 | }); 141 | 142 | const mapStateToProps = state => { 143 | return { 144 | themeColor: state.user.themeColor, 145 | }; 146 | }; 147 | 148 | export default connect(mapStateToProps)(AboutScreen); 149 | -------------------------------------------------------------------------------- /src/screen/tabs/HomeScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-16 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View} from 'react-native'; 6 | import {connect} from 'react-redux'; 7 | import { 8 | fetchHomeAddCollect, 9 | fetchHomeBanner, 10 | fetchHomeCancelCollect, 11 | fetchHomeList, 12 | fetchHomeListMore, 13 | } from '../../actions'; 14 | import NavBar from '../../component/NavBar'; 15 | import Banner from '../../component/Banner'; 16 | import globalStyles from '../../styles/globalStyles'; 17 | import {getRealDP as dp} from '../../utils/screenUtil'; 18 | import ListFooter from '../../component/ListFooter'; 19 | import ArticleItemRow from '../../component/ArticleItemRow'; 20 | import {i18n, showToast} from '../../utils/Utility'; 21 | import CommonFlatList from '../../component/CommonFlatList'; 22 | 23 | /** 24 | * 首页 25 | */ 26 | class HomeScreen extends PureComponent { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | isRefreshing: false, 31 | }; 32 | this.renderItem = this.renderItem.bind(this); 33 | this.renderHeader = this.renderHeader.bind(this); 34 | this.renderFooter = this.renderFooter.bind(this); 35 | this.onFetchData = this.onFetchData.bind(this); 36 | this.onRefresh = this.onRefresh.bind(this); 37 | this.onEndReached = this.onEndReached.bind(this); 38 | } 39 | 40 | async componentDidMount() { 41 | await this.onFetchData(); 42 | } 43 | 44 | async onFetchData() { 45 | await Promise.all([fetchHomeBanner(), fetchHomeList()]); 46 | } 47 | 48 | async onRefresh() { 49 | this.setState({isRefreshing: true}); 50 | await Promise.all([fetchHomeBanner(), fetchHomeList()]); 51 | this.setState({isRefreshing: false}); 52 | } 53 | 54 | onEndReached() { 55 | const {isFullData} = this.props; 56 | if (isFullData) { 57 | return; 58 | } 59 | fetchHomeListMore(this.props.page); 60 | } 61 | 62 | renderItem({item, index}) { 63 | const {navigation, isLogin} = this.props; 64 | return ( 65 | { 69 | if (!isLogin) { 70 | showToast(i18n('please-login-first')); 71 | return navigation.navigate('Login'); 72 | } 73 | if (item.collect) { 74 | fetchHomeCancelCollect(item.id, index); 75 | } else { 76 | fetchHomeAddCollect(item.id, index); 77 | } 78 | }} 79 | /> 80 | ); 81 | } 82 | 83 | renderHeader() { 84 | const {navigation, homeBanner} = this.props; 85 | return ( 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | 93 | renderFooter() { 94 | const {isRenderFooter, isFullData, themeColor} = this.props; 95 | return ( 96 | 101 | ); 102 | } 103 | 104 | render() { 105 | const {navigation, dataSource} = this.props; 106 | return ( 107 | 108 | navigation.toggleDrawer()} 114 | onRightPress={() => navigation.navigate('Search')} 115 | /> 116 | item.id.toString()} 119 | renderItem={this.renderItem} 120 | ListHeaderComponent={this.renderHeader} 121 | ListFooterComponent={this.renderFooter} 122 | onEndReached={this.onEndReached} 123 | isRefreshing={this.state.isRefreshing} 124 | toRefresh={this.onRefresh} 125 | /> 126 | 127 | ); 128 | } 129 | } 130 | 131 | const mapStateToProps = state => { 132 | return { 133 | page: state.home.page, 134 | dataSource: state.home.dataSource, 135 | homeBanner: state.home.homeBanner, 136 | homeList: state.home.homeList, 137 | isRenderFooter: state.home.isRenderFooter, 138 | isFullData: state.home.isFullData, 139 | isLogin: state.user.isLogin, 140 | themeColor: state.user.themeColor, 141 | language: state.user.language, 142 | }; 143 | }; 144 | 145 | export default connect(mapStateToProps)(HomeScreen); 146 | -------------------------------------------------------------------------------- /src/component/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import { 3 | Image, 4 | StyleSheet, 5 | Text, 6 | TouchableNativeFeedback, 7 | View, 8 | } from 'react-native'; 9 | import PropTypes from 'prop-types'; 10 | import {connect} from 'react-redux'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import { 13 | DEVICE_WIDTH, 14 | getRealDP as dp, 15 | getStatusBarHeight, 16 | isAndroid, 17 | } from '../utils/screenUtil'; 18 | import Color from '../utils/Color'; 19 | import Touchable from './Touchable'; 20 | import images from '../images'; 21 | 22 | const propTypes = { 23 | title: PropTypes.string, 24 | titleView: PropTypes.func, 25 | leftIcon: PropTypes.string, 26 | onLeftPress: PropTypes.func, 27 | rightIcon: PropTypes.string, 28 | onRightPress: PropTypes.func, 29 | navigation: PropTypes.object.isRequired, 30 | navBarStyle: PropTypes.oneOfType([ 31 | PropTypes.number, 32 | PropTypes.object, 33 | PropTypes.array, 34 | ]), 35 | }; 36 | const defaultProps = { 37 | title: '', 38 | leftIcon: 'md-arrow-back', 39 | noStatusBarHeight: false, 40 | navBarStyle: {}, 41 | }; 42 | 43 | const feedBackBackground = TouchableNativeFeedback.Ripple( 44 | 'rgba(50,50,50,0.3)', 45 | true, 46 | ); 47 | 48 | /** 49 | * NavBar 导航头组件 50 | */ 51 | class NavBar extends PureComponent { 52 | constructor(props) { 53 | super(props); 54 | this.state = {}; 55 | this.handleLeftBtnClick = this.handleLeftBtnClick.bind(this); 56 | } 57 | 58 | handleLeftBtnClick() { 59 | const {onLeftPress, navigation} = this.props; 60 | if (onLeftPress && typeof onLeftPress === 'function') { 61 | return onLeftPress(); 62 | } 63 | return navigation.goBack(); 64 | } 65 | 66 | render() { 67 | const { 68 | leftIcon, 69 | rightIcon, 70 | navBarStyle, 71 | titleView, 72 | title, 73 | onRightPress, 74 | isLogin, 75 | themeColor, 76 | } = this.props; 77 | return ( 78 | 80 | {/* 左侧按钮 */} 81 | 85 | 86 | {leftIcon === 'md-person' && isLogin ? ( 87 | 92 | ) : ( 93 | 94 | )} 95 | 96 | 97 | 98 | {/* 中间标题 */} 99 | {titleView ? ( 100 | titleView() 101 | ) : ( 102 | 103 | 104 | {title} 105 | 106 | 107 | )} 108 | 109 | {/* 右侧按钮 */} 110 | {rightIcon ? ( 111 | 115 | 116 | 117 | 118 | 119 | ) : ( 120 | 121 | )} 122 | 123 | ); 124 | } 125 | } 126 | 127 | const styles = StyleSheet.create({ 128 | container: { 129 | height: dp(100) + getStatusBarHeight(), 130 | flexDirection: 'row', 131 | alignItems: 'center', 132 | justifyContent: 'space-between', 133 | paddingTop: getStatusBarHeight(), 134 | paddingHorizontal: dp(28), 135 | }, 136 | titleWrapper: { 137 | width: DEVICE_WIDTH - dp(260), 138 | alignItems: 'center', 139 | }, 140 | title: { 141 | fontWeight: 'bold', 142 | fontSize: dp(38), 143 | color: Color.WHITE, 144 | }, 145 | iconWrapper: { 146 | height: dp(60), 147 | width: dp(60), 148 | borderRadius: dp(30), 149 | alignItems: 'center', 150 | justifyContent: 'center', 151 | }, 152 | myPhoto: { 153 | height: dp(58), 154 | width: dp(58), 155 | borderRadius: dp(29), 156 | borderWidth: dp(2), 157 | borderColor: Color.WHITE, 158 | }, 159 | }); 160 | 161 | NavBar.propTypes = propTypes; 162 | NavBar.defaultProps = defaultProps; 163 | 164 | const mapStateToProps = state => { 165 | return { 166 | isLogin: state.user.isLogin, 167 | themeColor: state.user.themeColor, 168 | language: state.user.language, 169 | }; 170 | }; 171 | 172 | export default connect(mapStateToProps)(NavBar); 173 | -------------------------------------------------------------------------------- /src/screen/tabs/SystemScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-16 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {StyleSheet, Text, View} from 'react-native'; 6 | import {connect} from 'react-redux'; 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import NavBar from '../../component/NavBar'; 9 | import Color from '../../utils/Color'; 10 | import globalStyles from '../../styles/globalStyles'; 11 | import {fetchSystemData} from '../../actions'; 12 | import {getRealDP as dp} from '../../utils/screenUtil'; 13 | import {getChapterBgColor, i18n} from '../../utils/Utility'; 14 | import Touchable from '../../component/Touchable'; 15 | import CommonFlatList from '../../component/CommonFlatList'; 16 | 17 | /** 18 | * 知识体系 19 | */ 20 | class SystemScreen extends PureComponent { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | isRefreshing: false, 25 | }; 26 | this.onRefresh = this.onRefresh.bind(this); 27 | this.renderItem = this.renderItem.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | fetchSystemData(); 32 | } 33 | 34 | async onRefresh() { 35 | this.setState({isRefreshing: true}); 36 | await fetchSystemData(); 37 | this.setState({isRefreshing: false}); 38 | } 39 | 40 | renderItem({item}) { 41 | const {navigation} = this.props; 42 | return ( 43 | { 45 | navigation.navigate('ArticleTab', { 46 | articleTabs: item.children, 47 | title: item.name, 48 | }); 49 | }}> 50 | 51 | 52 | {item.name} 53 | 54 | 55 | {item.children.map(el => ( 56 | 57 | 62 | {el.name} 63 | 64 | 65 | ))} 66 | 67 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | render() { 80 | const {navigation, systemData} = this.props; 81 | return ( 82 | 83 | navigation.toggleDrawer()} 89 | onRightPress={() => navigation.navigate('Search')} 90 | /> 91 | item.id.toString()} 94 | renderItem={this.renderItem} 95 | ListHeaderComponent={() => } 96 | isRefreshing={this.state.isRefreshing} 97 | toRefresh={this.onRefresh} 98 | /> 99 | 100 | ); 101 | } 102 | } 103 | 104 | const styles = StyleSheet.create({ 105 | itemWrapper: { 106 | flex: 1, 107 | alignItems: 'center', 108 | }, 109 | itemContent: { 110 | width: dp(700), 111 | borderRadius: dp(30), 112 | backgroundColor: Color.WHITE, 113 | alignItems: 'center', 114 | marginBottom: dp(20), 115 | paddingHorizontal: dp(20), 116 | paddingTop: dp(20), 117 | paddingBottom: dp(10), 118 | justifyContent: 'space-between', 119 | }, 120 | title: { 121 | fontSize: dp(32), 122 | color: Color.TEXT_MAIN, 123 | textAlign: 'center', 124 | fontWeight: 'bold', 125 | }, 126 | content: { 127 | flexDirection: 'row', 128 | justifyContent: 'space-between', 129 | alignItems: 'center', 130 | }, 131 | leftContent: { 132 | width: dp(620), 133 | flexWrap: 'wrap', 134 | flexDirection: 'row', 135 | paddingTop: dp(20), 136 | }, 137 | tabItemWrapper: { 138 | paddingHorizontal: dp(30), 139 | paddingVertical: dp(10), 140 | borderRadius: dp(30), 141 | marginRight: dp(15), 142 | marginBottom: dp(20), 143 | }, 144 | tabItemText: { 145 | fontSize: dp(28), 146 | color: Color.WHITE, 147 | }, 148 | }); 149 | 150 | const mapStateToProps = state => { 151 | return { 152 | systemData: state.system.systemData, 153 | themeColor: state.user.themeColor, 154 | language: state.user.language, 155 | }; 156 | }; 157 | 158 | export default connect(mapStateToProps)(SystemScreen); 159 | -------------------------------------------------------------------------------- /src/component/ArticleItemRow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-25 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View, Text, Image, StyleSheet} from 'react-native'; 6 | import PropTypes from 'prop-types'; 7 | import {getRealDP as dp} from '../utils/screenUtil'; 8 | import Color from '../utils/Color'; 9 | import globalStyles from '../styles/globalStyles'; 10 | import Touchable from './Touchable'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import {getChapterBgColor, i18n} from '../utils/Utility'; 13 | 14 | const propTypes = { 15 | navigation: PropTypes.object.isRequired, 16 | item: PropTypes.object.isRequired, 17 | isWxArticle: PropTypes.bool.isRequired, // 公众号文章 18 | onCollectPress: PropTypes.func.isRequired, // 收藏 19 | }; 20 | 21 | const defaultProps = { 22 | isWxArticle: false, 23 | onCollectPress: () => {}, 24 | }; 25 | 26 | class ArticleItemRow extends PureComponent { 27 | constructor(props) { 28 | super(props); 29 | this.state = {}; 30 | } 31 | render() { 32 | const {navigation, item, isWxArticle, onCollectPress} = this.props; 33 | return ( 34 | 37 | navigation.navigate('WebView', { 38 | title: item.title, 39 | url: item.link, 40 | }) 41 | }> 42 | 43 | 44 | 45 | {item.title} 46 | 47 | 48 | {item.desc} 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | 65 | 66 | {item.niceDate} 67 | 68 | {item.author || item.shareUser || item.chapterName} 69 | 70 | 71 | 72 | 73 | 74 | {item.envelopePic ? ( 75 | 80 | ) : ( 81 | 82 | 91 | 92 | {item.superChapterName || i18n('Article')} 93 | 94 | 95 | 96 | )} 97 | 98 | 99 | 100 | ); 101 | } 102 | } 103 | 104 | const styles = StyleSheet.create({ 105 | container: { 106 | flex: 1, 107 | alignItems: 'center', 108 | }, 109 | itemWrapper: { 110 | width: dp(700), 111 | backgroundColor: Color.WHITE, 112 | paddingHorizontal: dp(20), 113 | paddingTop: dp(25), 114 | paddingBottom: dp(13), 115 | flexDirection: 'row', 116 | justifyContent: 'space-between', 117 | marginBottom: dp(20), 118 | borderRadius: dp(20), 119 | }, 120 | itemLeftWrapper: { 121 | flex: 1, 122 | width: dp(520), 123 | justifyContent: 'space-between', 124 | }, 125 | itemRightWrapper: { 126 | justifyContent: 'center', 127 | }, 128 | image: { 129 | height: dp(200), 130 | width: dp(120), 131 | backgroundColor: Color.ICON_GRAY, 132 | }, 133 | title: { 134 | fontSize: dp(28), 135 | color: Color.TEXT_MAIN, 136 | fontWeight: 'bold', 137 | maxWidth: dp(520), 138 | }, 139 | desc: { 140 | fontSize: dp(26), 141 | color: Color.TEXT_DARK, 142 | marginVertical: dp(12), 143 | maxWidth: dp(520), 144 | }, 145 | likeTime: { 146 | width: dp(520), 147 | flexDirection: 'row', 148 | alignItems: 'center', 149 | }, 150 | dateText: { 151 | fontSize: dp(24), 152 | color: Color.TEXT_LIGHT, 153 | }, 154 | timeIconWrapper: { 155 | marginHorizontal: dp(20), 156 | }, 157 | }); 158 | 159 | ArticleItemRow.propTypes = propTypes; 160 | ArticleItemRow.defaultProps = defaultProps; 161 | 162 | export default ArticleItemRow; 163 | -------------------------------------------------------------------------------- /src/screen/drawer/CoinDetailScreen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-10-08 3 | */ 4 | import React, {PureComponent} from 'react'; 5 | import {View, StyleSheet, Text} from 'react-native'; 6 | import {connect} from 'react-redux'; 7 | import globalStyles from '../../styles/globalStyles'; 8 | import NavBar from '../../component/NavBar'; 9 | import { 10 | fetchMyCoinInfo, 11 | fetchMyCoinList, 12 | fetchMyCoinListMore, 13 | } from '../../actions'; 14 | import CommonFlatList from '../../component/CommonFlatList'; 15 | import { 16 | DEVICE_WIDTH, 17 | getBottomSpace, 18 | getRealDP as dp, 19 | } from '../../utils/screenUtil'; 20 | import ListFooter from '../../component/ListFooter'; 21 | import Color from '../../utils/Color'; 22 | import {i18n} from '../../utils/Utility'; 23 | /** 24 | * 积分明细列表 25 | */ 26 | class CoinDetailScreen extends PureComponent { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | isRefreshing: false, 31 | }; 32 | this.onRefresh = this.onRefresh.bind(this); 33 | this.onEndReached = this.onEndReached.bind(this); 34 | this.renderItem = this.renderItem.bind(this); 35 | this.renderHeader = this.renderHeader.bind(this); 36 | this.renderFooter = this.renderFooter.bind(this); 37 | } 38 | 39 | componentDidMount() { 40 | fetchMyCoinInfo(); 41 | fetchMyCoinList(); 42 | } 43 | 44 | async onRefresh() { 45 | this.setState({isRefreshing: true}); 46 | await fetchMyCoinList(); 47 | this.setState({isRefreshing: false}); 48 | } 49 | 50 | onEndReached() { 51 | const {isFullData} = this.props; 52 | if (isFullData) { 53 | return; 54 | } 55 | fetchMyCoinListMore(this.props.page); 56 | } 57 | 58 | renderItem({item}) { 59 | return ( 60 | 61 | 62 | {item.reason} 63 | {item.desc} 64 | 65 | {`+${item.coinCount}`} 66 | 67 | ); 68 | } 69 | 70 | renderHeader() { 71 | const {themeColor, coinCount} = this.props; 72 | return ( 73 | 74 | {coinCount} 75 | 76 | ); 77 | } 78 | 79 | renderFooter() { 80 | const {isRenderFooter, isFullData, themeColor} = this.props; 81 | return ( 82 | 87 | ); 88 | } 89 | 90 | renderSeparator() { 91 | return ( 92 | 93 | 94 | 95 | ); 96 | } 97 | 98 | render() { 99 | const {navigation, dataSource} = this.props; 100 | return ( 101 | 102 | { 107 | navigation.navigate('WebView', { 108 | title: i18n('points-rule'), 109 | url: 'https://www.wanandroid.com/blog/show/2653', 110 | }); 111 | }} 112 | /> 113 | item.id.toString()} 116 | renderItem={this.renderItem} 117 | ListHeaderComponent={this.renderHeader} 118 | ListFooterComponent={this.renderFooter} 119 | ItemSeparatorComponent={this.renderSeparator} 120 | onEndReached={this.onEndReached} 121 | isRefreshing={this.state.isRefreshing} 122 | toRefresh={this.onRefresh} 123 | /> 124 | 125 | ); 126 | } 127 | } 128 | 129 | const styles = StyleSheet.create({ 130 | container: { 131 | flex: 1, 132 | backgroundColor: Color.DEFAULT_BG, 133 | marginBottom: getBottomSpace(), 134 | }, 135 | headerWrapper: { 136 | height: dp(200), 137 | justifyContent: 'center', 138 | alignItems: 'center', 139 | }, 140 | coinText: { 141 | fontSize: dp(75), 142 | fontWeight: 'bold', 143 | color: Color.WHITE, 144 | }, 145 | itemWrapper: { 146 | width: DEVICE_WIDTH, 147 | flexDirection: 'row', 148 | backgroundColor: Color.WHITE, 149 | paddingVertical: dp(28), 150 | paddingHorizontal: dp(28), 151 | justifyContent: 'space-between', 152 | alignItems: 'center', 153 | }, 154 | coinCountText: { 155 | fontSize: dp(30), 156 | color: Color.WX_GREEN, 157 | fontWeight: 'bold', 158 | }, 159 | coinDescText: { 160 | fontSize: dp(28), 161 | color: Color.TEXT_DARK, 162 | marginTop: dp(20), 163 | }, 164 | coinReasonText: { 165 | fontSize: dp(30), 166 | color: Color.TEXT_MAIN, 167 | }, 168 | }); 169 | 170 | const mapStateToProps = state => { 171 | return { 172 | dataSource: state.coin.dataSource, 173 | isRenderFooter: state.coin.isRenderFooter, 174 | isFullData: state.coin.isFullData, 175 | page: state.coin.page, 176 | coinCount: state.coin.coinCount, 177 | themeColor: state.user.themeColor, 178 | }; 179 | }; 180 | 181 | export default connect(mapStateToProps)(CoinDetailScreen); 182 | -------------------------------------------------------------------------------- /src/component/ArticleFlatList.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {View} from 'react-native'; 3 | import PropTypes from 'prop-types'; 4 | import globalStyles from '../styles/globalStyles'; 5 | import {fetchWxArticleList, updateArticleLoading} from '../actions'; 6 | import {getRealDP as dp} from '../utils/screenUtil'; 7 | import ArticleItemRow from './ArticleItemRow'; 8 | import {connect} from 'react-redux'; 9 | import {addCollectArticle, cancelCollectArticle} from '../api'; 10 | import {i18n, showToast} from '../utils/Utility'; 11 | import ListFooter from './ListFooter'; 12 | import CommonFlatList from './CommonFlatList'; 13 | 14 | const propTypes = { 15 | chapterId: PropTypes.number.isRequired, 16 | navigation: PropTypes.object.isRequired, 17 | isWxArticle: PropTypes.bool, 18 | }; 19 | 20 | const defaultProps = { 21 | isWxArticle: false, 22 | }; 23 | 24 | /** 25 | * ArticleFlatList 26 | */ 27 | class ArticleFlatList extends PureComponent { 28 | constructor(props) { 29 | super(props); 30 | this.state = { 31 | isRefreshing: false, 32 | dataSource: [], 33 | isRenderFooter: true, 34 | isFullData: false, 35 | }; 36 | this.page = 1; 37 | this.renderFooter = this.renderFooter.bind(this); 38 | this.renderItem = this.renderItem.bind(this); 39 | this.onRefresh = this.onRefresh.bind(this); 40 | this.onEndReached = this.onEndReached.bind(this); 41 | } 42 | 43 | componentDidMount() { 44 | const {chapterId} = this.props; 45 | fetchWxArticleList(chapterId) 46 | .then(res => { 47 | updateArticleLoading(false); 48 | this.setState({dataSource: res.datas}); 49 | }) 50 | .catch(e => {}); 51 | } 52 | 53 | onRefresh() { 54 | const {chapterId} = this.props; 55 | this.page = 1; 56 | this.setState({isRefreshing: true}); 57 | fetchWxArticleList(chapterId) 58 | .then(res => { 59 | this.setState({ 60 | dataSource: res.datas, 61 | isRefreshing: false, 62 | isRenderFooter: !!res.total, // 只有total为0是不渲染footer 63 | isFullData: res.curPage === res.pageCount, 64 | }); 65 | }) 66 | .catch(e => { 67 | this.setState({isRefreshing: false}); 68 | }); 69 | } 70 | 71 | onEndReached() { 72 | const {chapterId} = this.props; 73 | const {dataSource, isFullData} = this.state; 74 | if (isFullData) { 75 | return; 76 | } 77 | fetchWxArticleList(chapterId, ++this.page) 78 | .then(res => { 79 | this.setState({ 80 | dataSource: dataSource.concat(res.datas), 81 | isRenderFooter: true, 82 | isFullData: !res.datas.length, 83 | }); 84 | }) 85 | .catch(e => { 86 | this.setState({isRefreshing: false}); 87 | }); 88 | } 89 | 90 | renderFooter() { 91 | const {themeColor} = this.props; 92 | return ( 93 | 98 | ); 99 | } 100 | 101 | renderItem({item, index}) { 102 | const {navigation, isWxArticle, isLogin} = this.props; 103 | return ( 104 | { 109 | if (!isLogin) { 110 | showToast(i18n('please-login-first')); 111 | return navigation.navigate('Login'); 112 | } 113 | if (item.collect) { 114 | cancelCollectArticle(item.id) 115 | .then(res => { 116 | let addCollectDataSource = [...this.state.dataSource]; 117 | addCollectDataSource[index].collect = false; 118 | this.setState({dataSource: addCollectDataSource}); 119 | }) 120 | .catch(e => {}); 121 | } else { 122 | addCollectArticle(item.id) 123 | .then(res => { 124 | showToast(i18n('Have been collected')); 125 | let addCollectDataSource = [...this.state.dataSource]; 126 | addCollectDataSource[index].collect = true; 127 | this.setState({dataSource: addCollectDataSource}); 128 | }) 129 | .catch(e => {}); 130 | } 131 | }} 132 | /> 133 | ); 134 | } 135 | 136 | render() { 137 | const {dataSource} = this.state; 138 | if (!dataSource.length) { 139 | return null; 140 | } 141 | return ( 142 | 143 | item.id.toString()} 146 | renderItem={this.renderItem} 147 | ListHeaderComponent={() => } 148 | ListFooterComponent={this.renderFooter} 149 | onEndReached={this.onEndReached} 150 | isRefreshing={this.state.isRefreshing} 151 | toRefresh={this.onRefresh} 152 | /> 153 | 154 | ); 155 | } 156 | } 157 | 158 | ArticleFlatList.propTypes = propTypes; 159 | ArticleFlatList.defaultProps = defaultProps; 160 | 161 | const mapStateToProps = state => { 162 | return { 163 | themeColor: state.user.themeColor, 164 | isLogin: state.user.isLogin, 165 | }; 166 | }; 167 | 168 | export default connect(mapStateToProps)(ArticleFlatList); 169 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid.xcodeproj/xcshareddata/xcschemes/RN_WanAndroid.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/actions/action-creator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by huangjunsheng on 2019-09-22 3 | */ 4 | import actionTypes from './actionType'; 5 | 6 | export function getHomeBannerAction(homeBanner) { 7 | return { 8 | type: actionTypes.FETCH_HOME_BANNER, 9 | homeBanner, 10 | }; 11 | } 12 | 13 | export function getHomeListAction(homeList) { 14 | return { 15 | type: actionTypes.FETCH_HOME_LIST, 16 | homeList, 17 | }; 18 | } 19 | 20 | export function getHomeListFailureAction() { 21 | return { 22 | type: actionTypes.FETCH_HOME_LIST_FAILURE, 23 | }; 24 | } 25 | 26 | export function getHomeListMoreAction(homeList) { 27 | return { 28 | type: actionTypes.FETCH_HOME_LIST_MORE, 29 | homeList, 30 | }; 31 | } 32 | 33 | export function getToRegisterAction(userInfo) { 34 | return { 35 | type: actionTypes.FETCH_TO_REGISTER, 36 | userInfo, 37 | }; 38 | } 39 | 40 | export function getToLoginAction(userInfo) { 41 | return { 42 | type: actionTypes.FETCH_TO_LOGIN, 43 | userInfo, 44 | }; 45 | } 46 | 47 | export function getToLoginFailureAction() { 48 | return { 49 | type: actionTypes.FETCH_TO_LOGIN_FAILURE, 50 | }; 51 | } 52 | 53 | export function getToLogoutAction() { 54 | return { 55 | type: actionTypes.FETCH_TO_LOGOUT, 56 | }; 57 | } 58 | 59 | export function getChangeThemeColorAction(themeColor) { 60 | return { 61 | type: actionTypes.CHANGE_THEME_COLOR, 62 | themeColor, 63 | }; 64 | } 65 | 66 | export function getInitialAuthInfoAction(initialInfo) { 67 | return { 68 | type: actionTypes.INITIAL_AUTH_INFO, 69 | initialInfo, 70 | }; 71 | } 72 | 73 | export function getSystemDataAction(systemData) { 74 | return { 75 | type: actionTypes.FETCH_TO_SYSTEM_DATA, 76 | systemData, 77 | }; 78 | } 79 | 80 | export function getWxArticleTabsAction(articleTabs) { 81 | return { 82 | type: actionTypes.FETCH_WX_ARTICLE_TABS, 83 | articleTabs, 84 | }; 85 | } 86 | 87 | export function getWxArticleListAction(articleList) { 88 | return { 89 | type: actionTypes.FETCH_WX_ARTICLE_LIST, 90 | articleList, 91 | }; 92 | } 93 | 94 | export function getGuideDataAction(guideData) { 95 | return { 96 | type: actionTypes.FETCH_GUIDE_DATA, 97 | guideData, 98 | }; 99 | } 100 | 101 | export function updateSelectIndexAction(selectIndex) { 102 | return { 103 | type: actionTypes.UPDATE_SELECT_INDEX, 104 | selectIndex, 105 | }; 106 | } 107 | 108 | export function getProjectTabsAction(projectTabs) { 109 | return { 110 | type: actionTypes.FETCH_PROJECT_TABS, 111 | projectTabs, 112 | }; 113 | } 114 | 115 | export function getArticleLoadingAction(isShowLoading) { 116 | return { 117 | type: actionTypes.FETCH_ARTICLE_LOADING, 118 | isShowLoading, 119 | }; 120 | } 121 | 122 | export function getOftenUsedWebsitesAction(websites) { 123 | return { 124 | type: actionTypes.FETCH_OFTEN_USED_WEBSITES, 125 | websites, 126 | }; 127 | } 128 | 129 | export function getSearchHotKeyAction(hotKey) { 130 | return { 131 | type: actionTypes.FETCH_SEARCH_HOT_KEY, 132 | hotKey, 133 | }; 134 | } 135 | 136 | export function getSearchArticleAction(articleData) { 137 | return { 138 | type: actionTypes.FETCH_SEARCH_ARTICLE, 139 | articleData, 140 | }; 141 | } 142 | 143 | export function getSearchArticleMoreAction(articleData) { 144 | return { 145 | type: actionTypes.FETCH_SEARCH_ARTICLE_MORE, 146 | articleData, 147 | }; 148 | } 149 | 150 | export function clearSearchArticleAction() { 151 | return { 152 | type: actionTypes.CLEAR_SEARCH_ARTICLE, 153 | }; 154 | } 155 | 156 | export function getCollectArticleListAction(collectData) { 157 | return { 158 | type: actionTypes.FETCH_COLLECT_ARTICLE, 159 | collectData, 160 | }; 161 | } 162 | 163 | export function getCollectArticleMoreAction(collectData) { 164 | return { 165 | type: actionTypes.FETCH_COLLECT_ARTICLE_MORE, 166 | collectData, 167 | }; 168 | } 169 | 170 | export function getHomeAddCollectAction(index) { 171 | return { 172 | type: actionTypes.FETCH_HOME_ADD_COLLECT, 173 | index, 174 | }; 175 | } 176 | 177 | export function getHomeCancelCollectAction(index) { 178 | return { 179 | type: actionTypes.FETCH_HOME_CANCEL_COLLECT, 180 | index, 181 | }; 182 | } 183 | 184 | export function getMyCollectCancelCollectAction(index) { 185 | return { 186 | type: actionTypes.FETCH_MYCOLLECT_CANCEL_COLLECT, 187 | index, 188 | }; 189 | } 190 | 191 | export function getMyCollectAddCollectAction(index) { 192 | return { 193 | type: actionTypes.FETCH_MYCOLLECT_ADD_COLLECT, 194 | index, 195 | }; 196 | } 197 | 198 | export function getSearchArticleAddCollectAction(index) { 199 | return { 200 | type: actionTypes.FETCH_SEARCH_ARTICLE_ADD_COLLECT, 201 | index, 202 | }; 203 | } 204 | 205 | export function getSearchArticleCancelCollectAction(index) { 206 | return { 207 | type: actionTypes.FETCH_SEARCH_ARTICLE_CANCEL_COLLECT, 208 | index, 209 | }; 210 | } 211 | 212 | export function getMyCoinListAction(coinList) { 213 | return { 214 | type: actionTypes.FETCH_MY_COIN_LIST, 215 | coinList, 216 | }; 217 | } 218 | 219 | export function getMyCoinListMoreAction(coinList) { 220 | return { 221 | type: actionTypes.FETCH_MY_COIN_LIST_MORE, 222 | coinList, 223 | }; 224 | } 225 | 226 | export function getMyCoinInfoAction(coinInfo) { 227 | return { 228 | type: actionTypes.FETCH_MY_COIN_INFO, 229 | coinInfo, 230 | }; 231 | } 232 | 233 | export function getSwitchAPPLanguageAction(language) { 234 | return { 235 | type: actionTypes.SWITCH_APP_LANGUAGE, 236 | language, 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /ios/RN_WanAndroid.xcodeproj/xcshareddata/xcschemes/RN_WanAndroid-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/screen/drawer/LoginScreen.js: -------------------------------------------------------------------------------- 1 | import React, {PureComponent} from 'react'; 2 | import {StyleSheet, Text, TextInput, View, ScrollView} from 'react-native'; 3 | import {connect} from 'react-redux'; 4 | import NavBar from '../../component/NavBar'; 5 | import {DEVICE_WIDTH, getRealDP as dp} from '../../utils/screenUtil'; 6 | import Color from '../../utils/Color'; 7 | import {i18n, showToast} from '../../utils/Utility'; 8 | import {fetchToLogin} from '../../actions'; 9 | import Icon from 'react-native-vector-icons/Ionicons'; 10 | import Touchable from '../../component/Touchable'; 11 | 12 | const propTypes = {}; 13 | const defaultProps = {}; 14 | 15 | /** 16 | * LoginScreen 17 | */ 18 | class LoginScreen extends PureComponent { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | userName: '', 23 | password: '', 24 | isSecure: true, 25 | }; 26 | this.toLogin = this.toLogin.bind(this); 27 | } 28 | 29 | toLogin() { 30 | const {navigation} = this.props; 31 | if (this.state.userName === '') { 32 | showToast(i18n('User name cannot be empty')); 33 | } else if (this.state.password === '') { 34 | showToast(i18n('Password cannot be empty')); 35 | } else { 36 | fetchToLogin( 37 | { 38 | username: this.state.userName, 39 | password: this.state.password, 40 | }, 41 | navigation, 42 | ); 43 | } 44 | } 45 | 46 | render() { 47 | const {navigation, themeColor} = this.props; 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | { 63 | this.setState({userName: text}); 64 | }} 65 | /> 66 | 67 | 68 | 69 | { 76 | this.setState({password: text}); 77 | }} 78 | /> 79 | { 84 | this.setState({isSecure: !this.state.isSecure}); 85 | }}> 86 | 91 | 92 | 93 | 96 | {i18n('login')} 97 | 98 | { 100 | navigation.navigate('Register'); 101 | }}> 102 | 103 | 104 | {i18n('New users? To')} 105 | 106 | {i18n('register')} 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | } 115 | 116 | const styles = StyleSheet.create({ 117 | container: { 118 | flex: 1, 119 | backgroundColor: Color.WHITE, 120 | }, 121 | content: { 122 | flex: 1, 123 | alignItems: 'center', 124 | paddingTop: dp(100), 125 | }, 126 | textInputWrapper: { 127 | flexDirection: 'row', 128 | alignItems: 'center', 129 | }, 130 | textInput: { 131 | width: DEVICE_WIDTH * 0.7, 132 | height: dp(80), 133 | margin: dp(20), 134 | borderColor: Color.ICON_GRAY, 135 | borderBottomWidth: dp(1), 136 | paddingLeft: dp(5), 137 | paddingRight: dp(50), 138 | color: Color.TEXT_MAIN, 139 | }, 140 | login: { 141 | width: DEVICE_WIDTH * 0.7, 142 | marginTop: dp(60), 143 | padding: dp(30), 144 | borderRadius: dp(50), 145 | }, 146 | loginText: { 147 | color: 'white', 148 | textAlign: 'center', 149 | fontSize: dp(32), 150 | }, 151 | eye: { 152 | position: 'absolute', 153 | right: dp(20), 154 | backgroundColor: Color.WHITE, 155 | padding: dp(5), 156 | }, 157 | register: { 158 | marginTop: dp(40), 159 | fontSize: dp(28), 160 | }, 161 | }); 162 | 163 | LoginScreen.propTypes = propTypes; 164 | LoginScreen.defaultProps = defaultProps; 165 | 166 | const mapStateToProps = state => { 167 | return { 168 | isLogin: state.user.isLogin, 169 | userInfo: state.user.userInfo, 170 | themeColor: state.user.themeColor, 171 | }; 172 | }; 173 | 174 | export default connect(mapStateToProps)(LoginScreen); 175 | --------------------------------------------------------------------------------