├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore └── Podfile ├── assets ├── screenshot │ ├── me.png │ ├── book.png │ ├── home.png │ ├── login.png │ ├── project.png │ ├── search.png │ ├── home_dark.png │ ├── knowledge.png │ ├── register.png │ ├── book_details.png │ ├── article_details.png │ ├── category_dark.png │ └── knowledge_dark.png └── images │ ├── ic_logo.png │ ├── ic_tab_me.png │ ├── ic_tab_book.png │ ├── ic_tab_home.png │ ├── ic_tab_project.png │ └── ic_tab_knowledge.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── zt │ │ │ │ │ └── flutter_wan_android │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── lib ├── core │ └── net │ │ ├── cancel │ │ ├── canceler.dart │ │ ├── zt_http_cancel.dart │ │ ├── cancel_manager.dart │ │ ├── http_canceler.dart │ │ └── http_cancel_manager.dart │ │ ├── http_config.dart │ │ ├── observer │ │ └── http_lifecycle_observer.dart │ │ ├── http_result.dart │ │ ├── convert │ │ └── json_converter.dart │ │ └── http_request.dart ├── config │ ├── hero_config.dart │ └── router_config.dart ├── utils │ ├── string_util.dart │ ├── log_util.dart │ ├── toast_util.dart │ ├── format_util.dart │ ├── shared_preferences_utils.dart │ └── screen_util.dart ├── base │ └── abs_dao.dart ├── modules │ ├── knowledge │ │ ├── model │ │ │ ├── knowledge_entity.dart │ │ │ ├── nav_entity.dart │ │ │ └── knowledge_model.dart │ │ ├── view_model │ │ │ ├── knowledge_view_model.dart │ │ │ └── knowledge_child_view_model.dart │ │ └── view │ │ │ └── main_knowledge.dart │ ├── book │ │ ├── model │ │ │ ├── study_entity.dart │ │ │ ├── book_entity.dart │ │ │ ├── book_model.dart │ │ │ └── study_dao.dart │ │ ├── view_model │ │ │ ├── book_view_model.dart │ │ │ └── book_details_view_model.dart │ │ └── view │ │ │ └── main_book.dart │ ├── home │ │ ├── model │ │ │ ├── banner_entity.dart │ │ │ └── home_model.dart │ │ ├── view_model │ │ │ └── home_view_model.dart │ │ └── widget │ │ │ └── banner_widget.dart │ ├── account │ │ ├── view_model │ │ │ ├── login_view_model.dart │ │ │ └── register_view_model.dart │ │ ├── model │ │ │ ├── user_entity.dart │ │ │ └── account_model.dart │ │ └── widget │ │ │ ├── login_button.dart │ │ │ └── login_text_field.dart │ ├── article │ │ ├── view_model │ │ │ └── article_details_view_model.dart │ │ ├── model │ │ │ └── article_entity.dart │ │ ├── widget │ │ │ └── item_article_widget.dart │ │ └── view │ │ │ └── article_details_page.dart │ ├── search │ │ ├── model │ │ │ ├── search_entity.dart │ │ │ ├── search_model.dart │ │ │ └── search_dao.dart │ │ └── view_model │ │ │ └── search_view_model.dart │ ├── project │ │ ├── model │ │ │ ├── category_entity.dart │ │ │ └── project_model.dart │ │ └── view_model │ │ │ ├── project_item_view_model.dart │ │ │ └── project_view_model.dart │ ├── main │ │ ├── view_model │ │ │ ├── locale_view_model.dart │ │ │ └── theme_view_model.dart │ │ └── view │ │ │ ├── preview_page.dart │ │ │ └── main_page.dart │ ├── collect │ │ ├── view_model │ │ │ └── collection_list_view_model.dart │ │ └── model │ │ │ └── collect_model.dart │ └── me │ │ └── view_model │ │ └── me_view_model.dart ├── generated │ ├── json │ │ ├── base │ │ │ ├── json_field.dart │ │ │ └── json_convert_content.dart │ │ ├── search_entity.g.dart │ │ ├── category_entity.g.dart │ │ ├── nav_entity.g.dart │ │ ├── banner_entity.g.dart │ │ ├── user_entity.g.dart │ │ ├── book_entity.g.dart │ │ └── article_entity.g.dart │ └── intl │ │ ├── messages_all.dart │ │ ├── messages_zh.dart │ │ └── messages_en.dart ├── widget │ ├── loading_dialog_helper.dart │ └── loading_dialog_widget.dart ├── l10n │ ├── intl_zh.arb │ └── intl_en.arb ├── helper │ ├── cookie_helper.dart │ ├── db_helper.dart │ ├── image_helper.dart │ └── router_helper.dart ├── common │ └── global_value.dart └── main.dart ├── .gitignore ├── test └── widget_test.dart ├── .metadata ├── analysis_options.yaml ├── pubspec.yaml └── README.md /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/screenshot/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/me.png -------------------------------------------------------------------------------- /assets/images/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_logo.png -------------------------------------------------------------------------------- /assets/images/ic_tab_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_tab_me.png -------------------------------------------------------------------------------- /assets/screenshot/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/book.png -------------------------------------------------------------------------------- /assets/screenshot/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/home.png -------------------------------------------------------------------------------- /assets/screenshot/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/login.png -------------------------------------------------------------------------------- /assets/images/ic_tab_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_tab_book.png -------------------------------------------------------------------------------- /assets/images/ic_tab_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_tab_home.png -------------------------------------------------------------------------------- /assets/screenshot/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/project.png -------------------------------------------------------------------------------- /assets/screenshot/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/search.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/images/ic_tab_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_tab_project.png -------------------------------------------------------------------------------- /assets/screenshot/home_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/home_dark.png -------------------------------------------------------------------------------- /assets/screenshot/knowledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/knowledge.png -------------------------------------------------------------------------------- /assets/screenshot/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/register.png -------------------------------------------------------------------------------- /assets/images/ic_tab_knowledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/images/ic_tab_knowledge.png -------------------------------------------------------------------------------- /assets/screenshot/book_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/book_details.png -------------------------------------------------------------------------------- /assets/screenshot/article_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/article_details.png -------------------------------------------------------------------------------- /assets/screenshot/category_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/category_dark.png -------------------------------------------------------------------------------- /assets/screenshot/knowledge_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/assets/screenshot/knowledge_dark.png -------------------------------------------------------------------------------- /lib/core/net/cancel/canceler.dart: -------------------------------------------------------------------------------- 1 | /// 取消器 2 | abstract class Canceler { 3 | ///取消 4 | void cancel({dynamic reason}); 5 | } 6 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/config/hero_config.dart: -------------------------------------------------------------------------------- 1 | ///Hero动画配置 2 | class HeroConfig { 3 | ///图片预览 4 | static const String tagPreview = "tagPreview"; 5 | } 6 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/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/RuffianZhong/FlutterApp/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/RuffianZhong/FlutterApp/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/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/RuffianZhong/FlutterApp/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /lib/utils/string_util.dart: -------------------------------------------------------------------------------- 1 | class StringUtil { 2 | ///移除html标签 3 | static String removeHtmlLabel(String data) { 4 | return data.replaceAll(RegExp('<[^>]+>'), ''); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/core/net/http_config.dart: -------------------------------------------------------------------------------- 1 | /// http 配置类 2 | class HttpConfig { 3 | static const String baseUrl = "https://www.wanandroid.com/"; //基础URL 4 | static const int timeout = 5000; //超时时间 5 | } 6 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuffianZhong/FlutterApp/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/core/net/cancel/zt_http_cancel.dart: -------------------------------------------------------------------------------- 1 | library zt_http_cancel; 2 | 3 | export 'cancel_manager.dart'; 4 | export 'canceler.dart'; 5 | export 'http_canceler.dart'; 6 | export 'http_cancel_manager.dart'; 7 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/zt/flutter_wan_android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zt.flutter_wan_android 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/utils/log_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | /// 日志工具类 4 | class Logger { 5 | static void log(Object? object) { 6 | if (kDebugMode) { 7 | print(object); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/* 2 | .idea/* 3 | build/* 4 | pubspec.lock 5 | .flutter-plugins 6 | .flutter-plugins-dependencies 7 | .packages 8 | flutter_wan_android.iml 9 | 10 | #ios 11 | ios/Pods/* 12 | ios/.symlinks/* 13 | ios/Podfile.lock 14 | 15 | #android 16 | android/.idea/* 17 | android/.gradle/* 18 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/base/abs_dao.dart: -------------------------------------------------------------------------------- 1 | import '../helper/db_helper.dart'; 2 | 3 | ///数据库操作相关Dao抽象基类 4 | abstract class AbsDao { 5 | ///数据库辅助类 6 | late SqliteHelper helper; 7 | 8 | AbsDao() { 9 | helper = SqliteHelper(); 10 | } 11 | 12 | ///使用完一定要关闭数据库 13 | void close() { 14 | helper.close(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/modules/knowledge/model/knowledge_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/knowledge/model/nav_entity.dart'; 2 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 3 | 4 | ///知识实体类 5 | class KnowledgeEntity { 6 | ///分类列表 7 | List categoryList = []; 8 | 9 | ///导航列表 10 | List navList = []; 11 | } 12 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/utils/toast_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | 5 | ///吐司工具类 6 | class ToastUtil { 7 | ///显示吐司 8 | static showToast( 9 | {required String msg, 10 | double? fontSize, 11 | ToastGravity? gravity, 12 | Color? backgroundColor, 13 | Color? textColor}) { 14 | Fluttertoast.showToast(msg: msg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/core/net/cancel/cancel_manager.dart: -------------------------------------------------------------------------------- 1 | import 'canceler.dart'; 2 | 3 | /// 取消管理类 4 | abstract class CancelManager { 5 | /// 添加/绑定取消器 6 | void bindCancel(Object object, Canceler canceler); 7 | 8 | /// 取消/移除cancel 9 | void cancel(Object object, [dynamic reason]); 10 | 11 | /// 取消/移除全部cancel 12 | void cancelAll(); 13 | 14 | /// 是否已经取消 15 | bool isCanceled(Object object); 16 | 17 | /// 获取canceler 18 | Canceler? getCanceler(Object object); 19 | } 20 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /lib/modules/book/model/study_entity.dart: -------------------------------------------------------------------------------- 1 | ///教程学习进度 2 | class StudyEntity { 3 | ///表ID 4 | int? id; 5 | 6 | ///教程ID 7 | late int bookId; 8 | 9 | ///文章ID:章节ID 10 | late int articleId; 11 | 12 | ///学习进度 13 | late double progress; 14 | 15 | ///学习时间 16 | late int time; 17 | 18 | StudyEntity( 19 | {this.id, 20 | required this.bookId, 21 | required this.articleId, 22 | required this.progress, 23 | required this.time}); 24 | 25 | Map toJson() => {}; 26 | } 27 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/modules/home/model/banner_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 2 | import 'package:flutter_wan_android/generated/json/banner_entity.g.dart'; 3 | import 'dart:convert'; 4 | 5 | @JsonSerializable() 6 | class BannerEntity { 7 | 8 | late int id; 9 | late String imagePath; 10 | late String title; 11 | late String url; 12 | 13 | BannerEntity(); 14 | 15 | factory BannerEntity.fromJson(Map json) => $BannerEntityFromJson(json); 16 | 17 | Map toJson() => $BannerEntityToJson(this); 18 | 19 | @override 20 | String toString() { 21 | return jsonEncode(this); 22 | } 23 | } -------------------------------------------------------------------------------- /lib/generated/json/base/json_field.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names 2 | // ignore_for_file: camel_case_types 3 | // ignore_for_file: prefer_single_quotes 4 | 5 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 6 | 7 | class JsonSerializable{ 8 | const JsonSerializable(); 9 | } 10 | 11 | class JSONField { 12 | //Specify the parse field name 13 | final String? name; 14 | 15 | //Whether to participate in toJson 16 | final bool? serialize; 17 | 18 | //Whether to participate in fromMap 19 | final bool? deserialize; 20 | 21 | const JSONField({this.name, this.serialize, this.deserialize}); 22 | } 23 | -------------------------------------------------------------------------------- /lib/widget/loading_dialog_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/widget/loading_dialog_widget.dart'; 3 | 4 | class LoadingDialogHelper { 5 | ///展示loading弹窗 6 | static void showLoading(BuildContext context, {bool dismissible = false}) { 7 | showDialog( 8 | barrierDismissible: dismissible, 9 | context: context, 10 | builder: (context) { 11 | return LoadingDialogWidget( 12 | dismissible: dismissible, 13 | ); 14 | }); 15 | } 16 | 17 | ///关闭弹窗 18 | static void dismissLoading(BuildContext context) { 19 | Navigator.pop(context); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/modules/account/view_model/login_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../model/account_model.dart'; 4 | 5 | class LoginViewModel extends ChangeNotifier { 6 | late AccountModel model; 7 | 8 | LoginViewModel() { 9 | model = AccountModel(); 10 | } 11 | 12 | ///允许登录 13 | bool _canLogin = false; 14 | 15 | bool get canLogin => _canLogin; 16 | 17 | set canLogin(bool value) { 18 | _canLogin = value; 19 | notifyListeners(); 20 | } 21 | 22 | ///密码模式 23 | bool _obscureText = true; 24 | 25 | bool get obscureText => _obscureText; 26 | 27 | set obscureText(bool value) { 28 | _obscureText = value; 29 | notifyListeners(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/modules/article/view_model/article_details_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 3 | 4 | ///ArticleDetailsViewModel 5 | class ArticleDetailsViewModel extends ChangeNotifier { 6 | ///加载进度 7 | double _loadProgress = 0; 8 | 9 | double get loadProgress => _loadProgress; 10 | 11 | set loadProgress(double value) { 12 | _loadProgress = value; 13 | notifyListeners(); 14 | } 15 | 16 | ///文章实体类 17 | ArticleEntity? _articleEntity; 18 | 19 | ArticleEntity? get articleEntity => _articleEntity; 20 | 21 | set articleEntity(ArticleEntity? value) { 22 | _articleEntity = value; 23 | notifyListeners(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/modules/search/model/search_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 4 | import 'package:flutter_wan_android/generated/json/search_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class SearchEntity { 8 | int? id; 9 | @JSONField(name: "name", serialize: true, deserialize: true) 10 | String? value = ""; 11 | int? time = 0; 12 | 13 | SearchEntity({this.id, this.value, this.time}); 14 | 15 | factory SearchEntity.fromJson(Map json) => 16 | $SearchEntityFromJson(json); 17 | 18 | Map toJson() => $SearchEntityToJson(this); 19 | 20 | @override 21 | String toString() { 22 | return jsonEncode(this); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/modules/project/model/category_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 4 | 5 | import '../../../generated/json/category_entity.g.dart'; 6 | 7 | ///类别实体 8 | @JsonSerializable() 9 | class CategoryEntity { 10 | ///一级分类ID 11 | late int id; 12 | late String name; 13 | 14 | ///二级分类列表 15 | @JSONField(name: 'children') 16 | List? childList; 17 | 18 | CategoryEntity(); 19 | 20 | factory CategoryEntity.fromJson(Map json) => 21 | $CategoryEntityFromJson(json); 22 | 23 | Map toJson() => $CategoryEntityToJson(this); 24 | 25 | @override 26 | String toString() { 27 | return jsonEncode(this); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/core/net/cancel/http_canceler.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 4 | import 'canceler.dart'; 5 | 6 | /// Http 消除器/取消管理者 7 | class HttpCanceler implements Canceler { 8 | final LifecycleOwner lifecycleOwner; //生命周期感知对象 9 | final CancelToken cancelToken; //dio 取消Token 10 | final LifecycleState lifecycleState; //widget生命周期状态 11 | 12 | HttpCanceler(this.lifecycleOwner, 13 | {CancelToken? cancelToken, LifecycleState? lifecycleState}) 14 | : cancelToken = cancelToken ?? CancelToken(), 15 | lifecycleState = lifecycleState ?? LifecycleState.onDestroy; 16 | 17 | @override 18 | void cancel({reason}) { 19 | cancelToken.cancel(reason); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/modules/knowledge/model/nav_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 4 | import 'package:flutter_wan_android/generated/json/nav_entity.g.dart'; 5 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 6 | 7 | ///导航实体类 8 | @JsonSerializable() 9 | class NavEntity { 10 | ///导航列表(文章列表) 11 | late List articles; 12 | 13 | ///导航ID 14 | late int cid; 15 | 16 | ///导航名称 17 | late String name; 18 | 19 | NavEntity(); 20 | 21 | factory NavEntity.fromJson(Map json) => 22 | $NavEntityFromJson(json); 23 | 24 | Map toJson() => $NavEntityToJson(this); 25 | 26 | @override 27 | String toString() { 28 | return jsonEncode(this); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/modules/main/view_model/locale_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/global_value.dart'; 4 | 5 | ///本地化ViewModel 6 | class LocaleViewModel extends ChangeNotifier { 7 | LocaleViewModel() { 8 | ///获取localIndex 9 | GlobalValue.getLocalIndex().then((value) { 10 | localIndex = value ?? 0; 11 | }); 12 | } 13 | 14 | ///本地化列表 15 | final List localList = ['zh', 'en']; 16 | 17 | ///本地化下标 18 | int _localIndex = 0; 19 | 20 | int get localIndex => _localIndex; 21 | 22 | set localIndex(int value) { 23 | _localIndex = value; 24 | notifyListeners(); 25 | } 26 | 27 | ///local 28 | Locale get locale => Locale(localList[localIndex]); 29 | 30 | ///存储local 31 | void setLocalIndex(int index) { 32 | GlobalValue.setLocalIndex(index); 33 | localIndex = index; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/utils/format_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | ///格式化工具类 4 | class FormatUtil { 5 | ///日期格式 6 | static const String ymdHms = "yyyy-MM-dd HH:mm:ss"; 7 | 8 | /// 格式化数值 9 | /// var f = NumberFormat("###.0#", "en_US"); 10 | /// print(f.format(12.345)); 11 | /// ==> 12.34 12 | static String formatNumber(String pattern, dynamic number, {String? locale}) { 13 | return NumberFormat(pattern, locale).format(number); 14 | } 15 | 16 | /// 格式化日期 17 | /// "yyyy-MM-dd HH:mm:ss" 18 | static String formatDate(String pattern, DateTime date) { 19 | return DateFormat(pattern).format(date); 20 | } 21 | 22 | /// 格式化毫秒日期 23 | /// "yyyy-MM-dd HH:mm:ss" 24 | static String formatMilliseconds(String pattern, int milliseconds) { 25 | return formatDate( 26 | pattern, DateTime.fromMillisecondsSinceEpoch(milliseconds)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/modules/account/view_model/register_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../model/account_model.dart'; 4 | 5 | class RegisterViewModel extends ChangeNotifier { 6 | late AccountModel model; 7 | 8 | RegisterViewModel() { 9 | model = AccountModel(); 10 | } 11 | 12 | ///允许注册 13 | bool _canRegister = false; 14 | 15 | bool get canRegister => _canRegister; 16 | 17 | set canRegister(bool value) { 18 | _canRegister = value; 19 | notifyListeners(); 20 | } 21 | 22 | ///密码模式 23 | bool _secretPsw = true; 24 | 25 | bool get secretPsw => _secretPsw; 26 | 27 | set secretPsw(bool value) { 28 | _secretPsw = value; 29 | notifyListeners(); 30 | } 31 | 32 | ///确认密码模式 33 | bool _secretPswConfirm = true; 34 | 35 | bool get secretPswConfirm => _secretPswConfirm; 36 | 37 | set secretPswConfirm(bool value) { 38 | _secretPswConfirm = value; 39 | notifyListeners(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/generated/json/search_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/search/model/search_entity.dart'; 3 | 4 | SearchEntity $SearchEntityFromJson(Map json) { 5 | final SearchEntity searchEntity = SearchEntity(); 6 | final int? id = jsonConvert.convert(json['id']); 7 | if (id != null) { 8 | searchEntity.id = id; 9 | } 10 | final String? value = jsonConvert.convert(json['name']); 11 | if (value != null) { 12 | searchEntity.value = value; 13 | } 14 | final int? time = jsonConvert.convert(json['time']); 15 | if (time != null) { 16 | searchEntity.time = time; 17 | } 18 | return searchEntity; 19 | } 20 | 21 | Map $SearchEntityToJson(SearchEntity entity) { 22 | final Map data = {}; 23 | data['id'] = entity.id; 24 | data['name'] = entity.value; 25 | data['time'] = entity.time; 26 | return data; 27 | } -------------------------------------------------------------------------------- /lib/core/net/observer/http_lifecycle_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 4 | 5 | import '../cancel/http_cancel_manager.dart'; 6 | import '../cancel/http_canceler.dart'; 7 | 8 | class HttpLifecycleObserver implements LifecycleObserver { 9 | /// http取消管理类 10 | final HttpCancelManager httpCancelManager; 11 | 12 | /// http 取消器 13 | /// 可以从 HttpCancelManager 中获取,此处认为 onStateChanged 回调频繁,通过成员变量的方式更优 14 | final HttpCanceler httpCanceler; 15 | 16 | HttpLifecycleObserver(this.httpCancelManager, this.httpCanceler); 17 | 18 | @override 19 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 20 | /// 目标组件,目标生命周期状态:取消网络请求 21 | if (httpCanceler.lifecycleOwner == owner && 22 | httpCanceler.lifecycleState == state) { 23 | httpCancelManager.cancel(owner); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/modules/account/model/user_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 4 | import 'package:flutter_wan_android/generated/json/user_entity.g.dart'; 5 | import 'package:flutter_wan_android/helper/image_helper.dart'; 6 | 7 | @JsonSerializable() 8 | class UserEntity { 9 | @JSONField(name: "id") 10 | int? uid = 0; 11 | String? nickname = ""; 12 | 13 | ///积分 14 | int? coinCount = 0; 15 | String? icon = ""; 16 | 17 | UserEntity(); 18 | 19 | factory UserEntity.fromJson(Map json) { 20 | ///手动添加头像 21 | if (!json.containsKey("icon")) { 22 | json["icon"] = ImageHelper.randomUrl(); 23 | } 24 | String value = json["icon"]; 25 | if (value.isEmpty) { 26 | json["icon"] = ImageHelper.randomUrl(); 27 | } 28 | return $UserEntityFromJson(json); 29 | } 30 | 31 | Map toJson() => $UserEntityToJson(this); 32 | 33 | @override 34 | String toString() { 35 | return jsonEncode(this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/modules/book/model/book_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 2 | import 'package:flutter_wan_android/generated/json/book_entity.g.dart'; 3 | import 'dart:convert'; 4 | 5 | ///教程实体类 6 | @JsonSerializable() 7 | class BookEntity { 8 | /* 9 | { 10 | "author": "阮一峰", 11 | "cover": "https://www.wanandroid.com/blogimgs/f1cb8d34-82c1-46f7-80fe-b899f56b69c1.png", 12 | "desc": "C 语言入门教程。", 13 | "id": 548, 14 | "lisense": "知识共享 署名-相同方式共享 3.0协议", 15 | "lisenseLink": "https://creativecommons.org/licenses/by-sa/3.0/deed.zh", 16 | "name": "C 语言入门教程_阮一峰" 17 | }*/ 18 | 19 | late String author; 20 | late String cover; 21 | late String desc; 22 | late int id; 23 | late String lisense; 24 | late String lisenseLink; 25 | late String name; 26 | 27 | BookEntity(); 28 | 29 | factory BookEntity.fromJson(Map json) => $BookEntityFromJson(json); 30 | 31 | Map toJson() => $BookEntityToJson(this); 32 | 33 | @override 34 | String toString() { 35 | return jsonEncode(this); 36 | } 37 | } -------------------------------------------------------------------------------- /lib/modules/main/view/preview_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/config/hero_config.dart'; 3 | import 'package:flutter_wan_android/helper/image_helper.dart'; 4 | 5 | import '../../../helper/router_helper.dart'; 6 | 7 | ///预览界面 8 | class PreviewPage extends StatefulWidget { 9 | final String imageUrl; 10 | 11 | const PreviewPage(this.imageUrl, {Key? key}) : super(key: key); 12 | 13 | @override 14 | State createState() => _PreviewPageState(); 15 | } 16 | 17 | class _PreviewPageState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Hero( 21 | tag: HeroConfig.tagPreview, 22 | child: GestureDetector( 23 | onTap: () => RouterHelper.pop(context), 24 | child: Container( 25 | alignment: Alignment.center, 26 | color: Colors.black, 27 | child: ImageHelper.network(widget.imageUrl, 28 | width: double.infinity, fit: BoxFit.fitWidth), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/generated/json/category_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 3 | 4 | CategoryEntity $CategoryEntityFromJson(Map json) { 5 | final CategoryEntity categoryEntity = CategoryEntity(); 6 | final int? id = jsonConvert.convert(json['id']); 7 | if (id != null) { 8 | categoryEntity.id = id; 9 | } 10 | final String? name = jsonConvert.convert(json['name']); 11 | if (name != null) { 12 | categoryEntity.name = name; 13 | } 14 | final List? childList = jsonConvert.convertListNotNull(json['children']); 15 | if (childList != null) { 16 | categoryEntity.childList = childList; 17 | } 18 | return categoryEntity; 19 | } 20 | 21 | Map $CategoryEntityToJson(CategoryEntity entity) { 22 | final Map data = {}; 23 | data['id'] = entity.id; 24 | data['name'] = entity.name; 25 | data['children'] = entity.childList?.map((v) => v.toJson()).toList(); 26 | return data; 27 | } -------------------------------------------------------------------------------- /lib/generated/json/nav_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/knowledge/model/nav_entity.dart'; 3 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 4 | 5 | 6 | NavEntity $NavEntityFromJson(Map json) { 7 | final NavEntity navEntity = NavEntity(); 8 | final List? articles = jsonConvert.convertListNotNull(json['articles']); 9 | if (articles != null) { 10 | navEntity.articles = articles; 11 | } 12 | final int? cid = jsonConvert.convert(json['cid']); 13 | if (cid != null) { 14 | navEntity.cid = cid; 15 | } 16 | final String? name = jsonConvert.convert(json['name']); 17 | if (name != null) { 18 | navEntity.name = name; 19 | } 20 | return navEntity; 21 | } 22 | 23 | Map $NavEntityToJson(NavEntity entity) { 24 | final Map data = {}; 25 | data['articles'] = entity.articles.map((v) => v.toJson()).toList(); 26 | data['cid'] = entity.cid; 27 | data['name'] = entity.name; 28 | return data; 29 | } -------------------------------------------------------------------------------- /lib/widget/loading_dialog_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///Loading弹窗 4 | ///网络请求等场景使用 5 | class LoadingDialogWidget extends StatelessWidget { 6 | bool dismissible = false; 7 | 8 | LoadingDialogWidget({Key? key, required this.dismissible}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Stack( 13 | alignment: Alignment.center, 14 | children: [ 15 | ///拦截返回导航 16 | WillPopScope( 17 | child: Container( 18 | decoration: BoxDecoration( 19 | borderRadius: BorderRadius.circular(4), 20 | color: Colors.grey[200]), 21 | padding: const EdgeInsets.all(20), 22 | height: 80, 23 | width: 80, 24 | child: CircularProgressIndicator( 25 | color: Theme.of(context).primaryColor, 26 | backgroundColor: Colors.grey[300], 27 | ), 28 | ), 29 | 30 | ///拦截返回按钮:false = 不允许通过返回按钮关闭弹窗 31 | onWillPop: () => Future.value(dismissible)) 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/generated/json/banner_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/home/model/banner_entity.dart'; 3 | 4 | BannerEntity $BannerEntityFromJson(Map json) { 5 | final BannerEntity bannerEntity = BannerEntity(); 6 | final int? id = jsonConvert.convert(json['id']); 7 | if (id != null) { 8 | bannerEntity.id = id; 9 | } 10 | final String? imagePath = jsonConvert.convert(json['imagePath']); 11 | if (imagePath != null) { 12 | bannerEntity.imagePath = imagePath; 13 | } 14 | final String? title = jsonConvert.convert(json['title']); 15 | if (title != null) { 16 | bannerEntity.title = title; 17 | } 18 | final String? url = jsonConvert.convert(json['url']); 19 | if (url != null) { 20 | bannerEntity.url = url; 21 | } 22 | return bannerEntity; 23 | } 24 | 25 | Map $BannerEntityToJson(BannerEntity entity) { 26 | final Map data = {}; 27 | data['id'] = entity.id; 28 | data['imagePath'] = entity.imagePath; 29 | data['title'] = entity.title; 30 | data['url'] = entity.url; 31 | return data; 32 | } -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_wan_android/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 17 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 18 | - platform: android 19 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 20 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 21 | - platform: ios 22 | create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 23 | base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /lib/generated/json/user_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/account/model/user_entity.dart'; 3 | import 'package:flutter_wan_android/helper/image_helper.dart'; 4 | 5 | 6 | UserEntity $UserEntityFromJson(Map json) { 7 | final UserEntity userEntity = UserEntity(); 8 | final int? uid = jsonConvert.convert(json['id']); 9 | if (uid != null) { 10 | userEntity.uid = uid; 11 | } 12 | final String? nickname = jsonConvert.convert(json['nickname']); 13 | if (nickname != null) { 14 | userEntity.nickname = nickname; 15 | } 16 | final int? coinCount = jsonConvert.convert(json['coinCount']); 17 | if (coinCount != null) { 18 | userEntity.coinCount = coinCount; 19 | } 20 | final String? icon = jsonConvert.convert(json['icon']); 21 | if (icon != null) { 22 | userEntity.icon = icon; 23 | } 24 | return userEntity; 25 | } 26 | 27 | Map $UserEntityToJson(UserEntity entity) { 28 | final Map data = {}; 29 | data['id'] = entity.uid; 30 | data['nickname'] = entity.nickname; 31 | data['coinCount'] = entity.coinCount; 32 | data['icon'] = entity.icon; 33 | return data; 34 | } -------------------------------------------------------------------------------- /lib/modules/collect/view_model/collection_list_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 3 | 4 | import '../../../core/net/cancel/http_canceler.dart'; 5 | import '../../../core/net/http_result.dart'; 6 | import '../model/collect_model.dart'; 7 | 8 | class CollectionListViewModel extends ChangeNotifier { 9 | CollectModel model = CollectModel(); 10 | 11 | ///数据页面下标 12 | int pageIndex = 0; 13 | 14 | ///文章列表 15 | List _articleList = []; 16 | 17 | List get articleList => _articleList; 18 | 19 | set articleList(List value) { 20 | _articleList = value; 21 | notifyListeners(); 22 | } 23 | 24 | ///获取内容列表 25 | Future> getArticleList( 26 | bool refresh, HttpCanceler canceler) async { 27 | ///下拉刷新,下标从0开始 28 | if (refresh) pageIndex = 0; 29 | HttpResult result = 30 | await model.getCollectList(pageIndex, canceler); 31 | if (result.success) { 32 | if (refresh) articleList.clear(); 33 | articleList.addAll(result.list!); 34 | articleList = articleList; 35 | pageIndex++; 36 | } 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/modules/knowledge/view_model/knowledge_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 3 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 4 | import 'package:flutter_wan_android/modules/knowledge/model/knowledge_entity.dart'; 5 | import 'package:flutter_wan_android/modules/knowledge/model/knowledge_model.dart'; 6 | 7 | class KnowledgeViewModel extends ChangeNotifier { 8 | KnowledgeModel model = KnowledgeModel(); 9 | 10 | ///知识实体类 11 | KnowledgeEntity _entity = KnowledgeEntity(); 12 | 13 | KnowledgeEntity get entity => _entity; 14 | 15 | set entity(KnowledgeEntity value) { 16 | _entity = value; 17 | notifyListeners(); 18 | } 19 | 20 | ///获取分类列表 21 | void getCategoryList(HttpCanceler canceler) { 22 | model.getSystemList(canceler).then((value) { 23 | if (value.success) { 24 | entity.categoryList = value.list!; 25 | entity = entity; 26 | } 27 | }); 28 | } 29 | 30 | ///获取导航列表 31 | void getNavList(HttpCanceler canceler) { 32 | model.getNavList(canceler).then((value) { 33 | if (value.success) { 34 | entity.navList = value.list!; 35 | entity = entity; 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/modules/book/view_model/book_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | 6 | import '../../../core/net/cancel/http_canceler.dart'; 7 | import '../../book/model/book_entity.dart'; 8 | import '../../book/model/book_model.dart'; 9 | 10 | class BookViewModel extends ChangeNotifier with LifecycleObserver { 11 | late HttpCanceler httpCanceler; 12 | BookModel model = BookModel(); 13 | 14 | ///教程列表 15 | List _dataArray = []; 16 | 17 | List get dataArray => _dataArray; 18 | 19 | set dataArray(List value) { 20 | _dataArray = value; 21 | notifyListeners(); 22 | } 23 | 24 | ///获取教程列表 25 | void getBookList() { 26 | model.getBookList(httpCanceler).then((value) { 27 | if (value.success) { 28 | dataArray = value.list!; 29 | } 30 | }); 31 | } 32 | 33 | @override 34 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 35 | if (state == LifecycleState.onInit) { 36 | httpCanceler = HttpCanceler(owner); 37 | } else if (state == LifecycleState.onCreate) { 38 | getBookList(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/modules/project/view_model/project_item_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/core/net/cancel/zt_http_cancel.dart'; 3 | import 'package:flutter_wan_android/core/net/http_result.dart'; 4 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 5 | import 'package:flutter_wan_android/modules/project/model/project_model.dart'; 6 | 7 | class ProjectItemViewModel extends ChangeNotifier { 8 | ProjectModel model = ProjectModel(); 9 | 10 | ///数据页面下标 11 | int pageIndex = 0; 12 | 13 | ///文章列表 14 | List _articleList = []; 15 | 16 | List get articleList => _articleList; 17 | 18 | set articleList(List value) { 19 | _articleList = value; 20 | notifyListeners(); 21 | } 22 | 23 | ///获取内容列表 24 | Future> getArticleList( 25 | int projectId, bool refresh, HttpCanceler canceler) async { 26 | ///下拉刷新,下标从0开始 27 | if (refresh) pageIndex = 0; 28 | HttpResult result = 29 | await model.getProjectList(projectId, pageIndex, canceler); 30 | if (result.success) { 31 | if (refresh) articleList.clear(); 32 | articleList.addAll(result.list!); 33 | articleList = articleList; 34 | pageIndex++; 35 | } 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/core/net/http_result.dart: -------------------------------------------------------------------------------- 1 | import '../../generated/json/base/json_convert_content.dart'; 2 | 3 | ///网络请求结果 4 | ///泛型是最终实体类型,如果想要 List 只需要指定 T 然后获取 list 对应的值即可 5 | class HttpResult { 6 | late int code; 7 | String? msg; 8 | 9 | ///解析成为实体类使用:与 list 互斥 10 | T? data; 11 | 12 | ///解析成为列表使用:与 data 互斥 13 | List? list; 14 | 15 | HttpResult(); 16 | 17 | ///业务逻辑是否成功 18 | bool get success => code == 0; 19 | 20 | HttpResult convert(Map json) { 21 | HttpResult entity = HttpResult(); 22 | 23 | /// 业务逻辑的 data 24 | dynamic data = json['data']; 25 | 26 | /// 业务逻辑的 code 27 | int code = json['errorCode']; 28 | 29 | /// 业务逻辑的 msg 30 | String? msg = json['errorMsg']; 31 | 32 | /// 解析成为 List 33 | if (data is List) { 34 | entity.list = convertList(data); 35 | } else { 36 | entity.data = convertData(data); 37 | } 38 | 39 | entity.code = code; 40 | entity.msg = msg; 41 | return entity; 42 | } 43 | 44 | ///转为List 45 | static List convertList(dynamic data) { 46 | List list = []; 47 | for (var item in data) { 48 | list.add(jsonConvert.convert(item) as M); 49 | } 50 | return list; 51 | } 52 | 53 | ///转为具体数据 54 | static M? convertData(dynamic data) { 55 | return jsonConvert.convert(data); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/l10n/intl_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "tab_home": "首页", 3 | "tab_project": "项目", 4 | "tab_knowledge": "知识", 5 | "tab_book": "教程", 6 | "tab_me": "我的", 7 | "integral": "积分:{value}", 8 | "collect": "收藏", 9 | "dark_style": "暗黑模式", 10 | "color_theme": "彩色主题", 11 | "settings": "设置", 12 | "placeholder": "--", 13 | "login": "登录", 14 | "register": "注册", 15 | "register_now": "立即注册", 16 | "no_account": "还没账号?", 17 | "user_name": "用户名", 18 | "user_psw": "密码", 19 | "user_psw_confirm": "确认密码", 20 | "topping": "置顶", 21 | "label_group": "{main} - {sub}", 22 | "search_hint": "用空格分隔多个关键词", 23 | "search_hot_title": "热门搜索", 24 | "search_local_title": "历史搜索", 25 | "edit": "编辑", 26 | "clean_all": "清除全部", 27 | "done": "完成", 28 | "loading_content": "内容加载中...", 29 | "learn_progress": "已学{progress}%", 30 | "learn_no": "未学习", 31 | "multi_language": "多语言", 32 | "language_chinese": "中文", 33 | "language_english": "英文", 34 | "account_empty_tip": "请输入账号", 35 | "psw_empty_tip": "请输入密码", 36 | "psw_confirm_empty_tip": "请确认密码", 37 | "psw_confirm_tip": "两次密码不一致", 38 | "login_success": "登录成功", 39 | "register_success": "注册成功", 40 | "net_error": "网络错误", 41 | "tab_tree": "体系", 42 | "tab_nav": "导航", 43 | "tab_book_course": "书籍教程", 44 | "tips_msg": "提示", 45 | "cancel": "取消", 46 | "confirm": "确定", 47 | "collect_content": "您确定要移除收藏内容吗?" 48 | } -------------------------------------------------------------------------------- /lib/modules/knowledge/view_model/knowledge_child_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 3 | import 'package:flutter_wan_android/core/net/http_result.dart'; 4 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 5 | import 'package:flutter_wan_android/modules/knowledge/model/knowledge_model.dart'; 6 | 7 | class KnowledgeChildViewModel extends ChangeNotifier { 8 | KnowledgeModel model = KnowledgeModel(); 9 | 10 | ///数据页面下标 11 | int pageIndex = 0; 12 | 13 | ///文章列表 14 | List _articleList = []; 15 | 16 | List get articleList => _articleList; 17 | 18 | set articleList(List value) { 19 | _articleList = value; 20 | notifyListeners(); 21 | } 22 | 23 | ///获取内容列表 24 | Future> getArticleList( 25 | int projectId, bool refresh, HttpCanceler canceler) async { 26 | ///下拉刷新,下标从0开始 27 | if (refresh) pageIndex = 0; 28 | HttpResult result = 29 | await model.getCategoryArticleList(projectId, pageIndex, canceler); 30 | if (result.success) { 31 | if (refresh) articleList.clear(); 32 | articleList.addAll(result.list!); 33 | articleList = articleList; 34 | pageIndex++; 35 | } 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/core/net/cancel/http_cancel_manager.dart: -------------------------------------------------------------------------------- 1 | import 'cancel_manager.dart'; 2 | import 'canceler.dart'; 3 | import 'http_canceler.dart'; 4 | 5 | /// Http 取消管理类 6 | class HttpCancelManager implements CancelManager { 7 | /// http 取消器集合 8 | Map map = {}; 9 | 10 | @override 11 | void bindCancel(Object object, Canceler canceler) { 12 | map[object] = canceler as HttpCanceler; 13 | } 14 | 15 | @override 16 | bool isCanceled(Object object) { 17 | if (map.isEmpty) return true; 18 | HttpCanceler canceler = map[object] as HttpCanceler; 19 | return canceler.cancelToken.isCancelled; 20 | } 21 | 22 | @override 23 | void cancelAll() { 24 | if (map.isEmpty) return; 25 | 26 | /// 移除所有请求 27 | map.forEach((key, value) { 28 | cancel(key); 29 | }); 30 | } 31 | 32 | @override 33 | void cancel(Object object, [dynamic reason]) { 34 | if (map.isEmpty) return; 35 | if (map[object] == null) return; 36 | 37 | /// 取消网络请求 38 | HttpCanceler canceler = map[object] as HttpCanceler; 39 | if (!canceler.cancelToken.isCancelled) { 40 | ///依据生命周期取消 41 | canceler.cancelToken.cancel(reason ?? "canceled base on lifecycle."); 42 | } 43 | 44 | /// 移除对象 45 | map.remove(object); 46 | } 47 | 48 | @override 49 | Canceler? getCanceler(Object object) { 50 | return map[object] as HttpCanceler; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/helper/cookie_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cookie_jar/cookie_jar.dart'; 4 | import 'package:flutter_wan_android/core/net/http_config.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | ///网络请求Cookie辅助类 8 | class CookieHelper { 9 | ///持久化到文件的cookie 10 | static PersistCookieJar? _cookieJar; 11 | 12 | static Future get cookieJar async { 13 | if (_cookieJar == null) { 14 | Directory appDocDir = await getApplicationDocumentsDirectory(); 15 | String appDocPath = appDocDir.path; 16 | 17 | _cookieJar = PersistCookieJar( 18 | ignoreExpires: true, storage: FileStorage("$appDocPath/.cookies/")); 19 | } 20 | 21 | return _cookieJar!; 22 | } 23 | 24 | ///保存cookie 25 | static void saveCookie(Map json) async { 26 | /* List cookies = [ 27 | Cookie("userLoginName", "zhong01"), 28 | Cookie("userLoginPassword", "test12345"), 29 | ];*/ 30 | 31 | List cookies = []; 32 | json.forEach((key, value) { 33 | cookies.add(Cookie(key, value)); 34 | }); 35 | (await cookieJar).saveFromResponse(Uri.parse(HttpConfig.baseUrl), cookies); 36 | } 37 | 38 | ///获取cookie 39 | static Future> getCookie() async { 40 | List cookies = 41 | await (await cookieJar).loadForRequest(Uri.parse(HttpConfig.baseUrl)); 42 | return cookies; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/modules/project/view_model/project_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 6 | import 'package:flutter_wan_android/modules/project/model/project_model.dart'; 7 | 8 | import '../../../core/net/cancel/http_canceler.dart'; 9 | 10 | class ProjectViewModel extends ChangeNotifier with LifecycleObserver { 11 | late HttpCanceler httpCanceler; 12 | ProjectModel model = ProjectModel(); 13 | 14 | ///项目分类列表 15 | List _projectList = []; 16 | 17 | List get projectList => _projectList; 18 | 19 | set projectList(List value) { 20 | _projectList = value; 21 | notifyListeners(); 22 | } 23 | 24 | void getProjectTree() { 25 | model.getProjectTree(canceler: httpCanceler).then((value) { 26 | if (value.success) { 27 | projectList = value.list!; 28 | } 29 | }); 30 | } 31 | 32 | @override 33 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 34 | if (state == LifecycleState.onInit) { 35 | httpCanceler = HttpCanceler(owner); 36 | } else if (state == LifecycleState.onCreate) { 37 | getProjectTree(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /lib/modules/account/widget/login_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///登录按钮 4 | class LoginButton extends StatefulWidget { 5 | final String text; 6 | final bool canSubmit; 7 | final VoidCallback onPressed; 8 | 9 | const LoginButton( 10 | {Key? key, 11 | required this.text, 12 | required this.canSubmit, 13 | required this.onPressed}) 14 | : super(key: key); 15 | 16 | @override 17 | State createState() => _LoginButtonState(); 18 | } 19 | 20 | class _LoginButtonState extends State { 21 | @override 22 | Widget build(BuildContext context) { 23 | return TextButton( 24 | onPressed: widget.canSubmit ? widget.onPressed : null, 25 | style: ButtonStyle( 26 | ///背景 27 | backgroundColor: MaterialStateProperty.resolveWith((states) { 28 | //不可用状态 29 | if (states.contains(MaterialState.disabled)) { 30 | return Colors.grey[400]; 31 | } 32 | 33 | //默认状态 34 | return Theme.of(context).primaryColor; 35 | }), 36 | 37 | ///前景:字体 38 | foregroundColor: MaterialStateProperty.resolveWith((states) { 39 | //默认状态 40 | return Colors.white; 41 | }), 42 | 43 | ///形状 44 | shape: MaterialStateProperty.all( 45 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(50))), 46 | 47 | ///宽高:double.infinity填充父布局 48 | minimumSize: 49 | MaterialStateProperty.all(const Size(double.infinity, 50.0)), 50 | ), 51 | child: Text(widget.text), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/modules/project/model/project_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 2 | import 'package:flutter_wan_android/core/net/http_result.dart'; 3 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 4 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 5 | 6 | import '../../../core/net/http_request.dart'; 7 | 8 | class ProjectModel { 9 | ///项目分类 10 | final String projectTreeApi = "project/tree/json"; 11 | 12 | ///项目列表数据 13 | final String projectListApi = "project/list/%1/json?cid=%2"; 14 | 15 | ///获取项目分配 16 | Future> getProjectTree( 17 | {HttpCanceler? canceler}) async { 18 | ///结果 19 | Map json = 20 | await HttpRequest.get(projectTreeApi, canceler: canceler); 21 | 22 | ///解析 23 | HttpResult result = 24 | HttpResult().convert(json); 25 | 26 | return result; 27 | } 28 | 29 | ///获取项目列表 30 | ///projectId:项目分类ID 31 | Future> getProjectList( 32 | int projectId, int pageIndex, HttpCanceler canceler) async { 33 | String api = projectListApi.replaceAll('%1', pageIndex.toString()); 34 | api = api.replaceAll('%2', projectId.toString()); 35 | 36 | ///结果 37 | Map json = await HttpRequest.get(api, canceler: canceler); 38 | 39 | ///解析 40 | HttpResult result = 41 | HttpResult().convert(json); 42 | 43 | result.list = HttpResult.convertList(json['data']["datas"]); 44 | 45 | return result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/generated/json/book_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/book/model/book_entity.dart'; 3 | 4 | BookEntity $BookEntityFromJson(Map json) { 5 | final BookEntity bookEntity = BookEntity(); 6 | final String? author = jsonConvert.convert(json['author']); 7 | if (author != null) { 8 | bookEntity.author = author; 9 | } 10 | final String? cover = jsonConvert.convert(json['cover']); 11 | if (cover != null) { 12 | bookEntity.cover = cover; 13 | } 14 | final String? desc = jsonConvert.convert(json['desc']); 15 | if (desc != null) { 16 | bookEntity.desc = desc; 17 | } 18 | final int? id = jsonConvert.convert(json['id']); 19 | if (id != null) { 20 | bookEntity.id = id; 21 | } 22 | final String? lisense = jsonConvert.convert(json['lisense']); 23 | if (lisense != null) { 24 | bookEntity.lisense = lisense; 25 | } 26 | final String? lisenseLink = jsonConvert.convert(json['lisenseLink']); 27 | if (lisenseLink != null) { 28 | bookEntity.lisenseLink = lisenseLink; 29 | } 30 | final String? name = jsonConvert.convert(json['name']); 31 | if (name != null) { 32 | bookEntity.name = name; 33 | } 34 | return bookEntity; 35 | } 36 | 37 | Map $BookEntityToJson(BookEntity entity) { 38 | final Map data = {}; 39 | data['author'] = entity.author; 40 | data['cover'] = entity.cover; 41 | data['desc'] = entity.desc; 42 | data['id'] = entity.id; 43 | data['lisense'] = entity.lisense; 44 | data['lisenseLink'] = entity.lisenseLink; 45 | data['name'] = entity.name; 46 | return data; 47 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "tab_home": "Home", 3 | "tab_project": "Project", 4 | "tab_knowledge": "Knowledge", 5 | "tab_book": "Book", 6 | "tab_me": "Me", 7 | "integral": "Integral:{value}", 8 | "collect": "Collect", 9 | "dark_style": "Dark Mode", 10 | "color_theme": "Color Theme", 11 | "settings": "Settings", 12 | "placeholder": "--", 13 | "login": "Login", 14 | "register": "Register", 15 | "register_now": "Register now", 16 | "no_account": "No account yet?", 17 | "user_name": "UserName", 18 | "user_psw": "Password", 19 | "user_psw_confirm": "Confirm Password", 20 | "topping": "Topping", 21 | "label_group": "{main} - {sub}", 22 | "search_hint": "separate multiple keywords with spaces", 23 | "search_hot_title": "Popular search", 24 | "search_local_title": "Historical search", 25 | "edit": "Edit", 26 | "clean_all": "Clear all", 27 | "done": "Done", 28 | "loading_content": "content loading...", 29 | "learn_progress": "Learned{progress}%", 30 | "learn_no": "No learned", 31 | "multi_language": "Multi language", 32 | "language_chinese": "Chinese", 33 | "language_english": "English", 34 | "account_empty_tip": "Please enter an account", 35 | "psw_empty_tip": "Please enter an password", 36 | "psw_confirm_empty_tip": "Please confirm the password", 37 | "psw_confirm_tip": "Two passwords are inconsistent", 38 | "login_success": "Login success", 39 | "register_success": "Register success", 40 | "net_error": "Network error", 41 | "tab_tree": "System", 42 | "tab_nav": "Navigation", 43 | "tab_book_course": "Book tutorial", 44 | "tips_msg": "Tips", 45 | "cancel": "Cancel", 46 | "confirm": "Confirm", 47 | "collect_content": "Are you sure remove the collected content?" 48 | } -------------------------------------------------------------------------------- /lib/modules/article/model/article_entity.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/generated/json/article_entity.g.dart'; 4 | import 'package:flutter_wan_android/generated/json/base/json_field.dart'; 5 | import 'package:flutter_wan_android/helper/image_helper.dart'; 6 | import 'package:flutter_wan_android/modules/book/model/study_entity.dart'; 7 | 8 | import '../../../generated/json/base/json_convert_content.dart'; 9 | 10 | @JsonSerializable() 11 | class ArticleEntity { 12 | late int id; 13 | @JSONField(name: "shareUser") 14 | String? userName; 15 | String? userIcon; 16 | String? link; 17 | String? title; 18 | @JSONField(name: "niceDate") 19 | String? date; 20 | 21 | ///主分类 22 | String? superChapterName; 23 | 24 | ///副级分类 25 | String? chapterName; 26 | 27 | ///文章可能是在多级,此字段是文章所属的直系分类ID 28 | int? chapterId; 29 | 30 | ///简介,副标题 31 | String? desc; 32 | 33 | ///封面 34 | @JSONField(name: "envelopePic") 35 | String? cover; 36 | 37 | ///是否置顶 38 | bool? isTop = false; 39 | 40 | ///是否收藏 41 | bool? collect = false; 42 | 43 | ///学习进度:不参与自动解析 44 | //@JSONField(name: "", deserialize: false, serialize: false) 45 | StudyEntity? study; 46 | 47 | ArticleEntity(); 48 | 49 | factory ArticleEntity.fromJson(Map json) { 50 | ArticleEntity entity = $ArticleEntityFromJson(json); 51 | entity.userIcon = ImageHelper.randomUrl(); 52 | if (entity.userName == null || entity.userName == "") { 53 | entity.userName = jsonConvert.convert(json['author']); 54 | } 55 | return entity; 56 | } 57 | 58 | Map toJson() => $ArticleEntityToJson(this); 59 | 60 | @override 61 | String toString() { 62 | return jsonEncode(this); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/config/router_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/modules/account/view/login_page.dart'; 3 | import 'package:flutter_wan_android/modules/account/view/register_page.dart'; 4 | import 'package:flutter_wan_android/modules/article/view/article_details_page.dart'; 5 | import 'package:flutter_wan_android/modules/book/view/book_details_page.dart'; 6 | import 'package:flutter_wan_android/modules/collect/view/collection_list_page.dart'; 7 | import 'package:flutter_wan_android/modules/knowledge/view/knowledge_child_page.dart'; 8 | import 'package:flutter_wan_android/modules/search/view/search_page.dart'; 9 | 10 | ///路由配置 11 | class RouterConfig { 12 | ///搜索页面 13 | static const String searchPage = "searchPage"; 14 | 15 | ///内容页面 16 | static const String articleDetailsPage = "articleDetailsPage"; 17 | 18 | ///登录页面 19 | static const String loginPage = "loginPage"; 20 | 21 | ///注册页面 22 | static const String registerPage = "registerPage"; 23 | 24 | ///知识体系二级界面 25 | static const String knowledgeChildPage = "knowledgeChildPage"; 26 | 27 | ///收藏列表页面 28 | static const String collectListPage = "collectListPage"; 29 | 30 | ///教程详情页面 31 | static const String bookDetailsPage = "bookDetailsPage"; 32 | 33 | ///路由表配置 34 | static Map routes = { 35 | searchPage: (context) => const SearchPage(), 36 | articleDetailsPage: (context) => const ArticleDetailsPage(), 37 | loginPage: (context) => const LoginPage(), 38 | registerPage: (context) => const RegisterPage(), 39 | knowledgeChildPage: (context) => const KnowledgeChildPage(), 40 | collectListPage: (context) => const CollectionListPage(), 41 | bookDetailsPage: (context) => const BookDetailsPage(), 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/modules/collect/model/collect_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 2 | 3 | import '../../../core/net/cancel/http_canceler.dart'; 4 | import '../../../core/net/http_request.dart'; 5 | import '../../../core/net/http_result.dart'; 6 | 7 | class CollectModel { 8 | ///收藏列表 9 | final String collectListApi = "lg/collect/list/%1/json"; 10 | 11 | ///收藏文章 12 | final String collectArticleApi = "lg/collect/%1/json"; 13 | 14 | ///取消收藏 15 | 16 | final String unCollectArticleApi = "lg/uncollect_originId/%1/json"; 17 | 18 | ///获取收藏文章列表 19 | Future> getCollectList( 20 | int pageIndex, HttpCanceler canceler) async { 21 | ///参数 22 | String api = collectListApi.replaceAll("%1", pageIndex.toString()); 23 | 24 | ///结果 25 | Map json = await HttpRequest.get(api, canceler: canceler); 26 | 27 | ///解析 28 | HttpResult result = 29 | HttpResult().convert(json); 30 | 31 | if (result.success) { 32 | result.list = 33 | HttpResult.convertList(json['data']["datas"]); 34 | for (ArticleEntity entity in result.list!) { 35 | ///收藏列表默认都是已经收藏的文章 36 | entity.collect = true; 37 | } 38 | } 39 | 40 | return result; 41 | } 42 | 43 | ///收藏或取消文章 44 | Future collectOrCancelArticle(int articleId, bool collect, 45 | {HttpCanceler? canceler}) async { 46 | ///参数 47 | String api = collect ? collectArticleApi : unCollectArticleApi; 48 | 49 | api = api.replaceAll("%1", articleId.toString()); 50 | 51 | ///结果 52 | Map json = await HttpRequest.post(api, canceler: canceler); 53 | 54 | ///解析 55 | HttpResult result = HttpResult().convert(json); 56 | 57 | return result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/modules/book/model/book_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 2 | import 'package:flutter_wan_android/modules/book/model/study_dao.dart'; 3 | import 'package:flutter_wan_android/modules/book/model/study_entity.dart'; 4 | import 'package:flutter_wan_android/modules/knowledge/model/knowledge_model.dart'; 5 | 6 | import '../../../core/net/cancel/http_canceler.dart'; 7 | import '../../../core/net/http_request.dart'; 8 | import '../../../core/net/http_result.dart'; 9 | import 'book_entity.dart'; 10 | 11 | class BookModel { 12 | ///教程列表 13 | final String bookListApi = "chapter/547/sublist/json"; 14 | 15 | ///获取教程列表f 16 | Future> getBookList(HttpCanceler? canceler) async { 17 | ///结果 18 | Map json = 19 | await HttpRequest.get(bookListApi, canceler: canceler); 20 | 21 | ///解析 22 | HttpResult result = HttpResult().convert(json); 23 | 24 | return result; 25 | } 26 | 27 | ///获取项目列表 28 | ///projectId:项目分类ID 29 | Future> getBookArticleList( 30 | int projectId, int pageIndex, HttpCanceler canceler) async { 31 | ///逻辑与获取分类下文章一致,直接复用 32 | 33 | KnowledgeModel model = KnowledgeModel(); 34 | return model.getCategoryArticleList(projectId, pageIndex, canceler); 35 | } 36 | 37 | StudyDao dao = StudyDao(); 38 | 39 | void close() { 40 | dao.close(); 41 | } 42 | 43 | ///插入或更新数据 44 | Future insertOrUpdateStudy( 45 | int bookId, int articleId, double progress, 46 | {int? id}) async { 47 | return dao.insertOrUpdate( 48 | id: id, bookId: bookId, articleId: articleId, progress: progress); 49 | } 50 | 51 | ///获取学习数据 52 | Future?> getStudyList(int bookId) async { 53 | return dao.query(bookId); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Flutter Wan Android 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | flutter_wan_android 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_wan_android 2 | description: A Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # 版本号:外部版本号 + 内部协议版本号 9 | version: 1.0.0+1 10 | 11 | environment: 12 | sdk: ">=2.17.5 <3.0.4" 13 | 14 | #强制解决依赖冲突 15 | dependency_overrides: 16 | path_drawing: ^0.5.1 17 | 18 | # 项目依赖:三方库依赖 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # 系统widget国际化支持 24 | flutter_localizations: 25 | sdk: flutter 26 | 27 | # 创建项目自带 28 | cupertino_icons: ^1.0.2 29 | 30 | # dio网络库 31 | dio: ^4.0.6 32 | # 键盘可见性 33 | flutter_keyboard_visibility: ^5.3.0 34 | # 异步库 35 | async: ^2.8.2 36 | # 本地配置数据存储 37 | shared_preferences: ^2.0.15 38 | # 文件读写(文件夹路径) 39 | path_provider: ^2.0.11 40 | # sqlite数据库 41 | sqflite: ^2.0.2+1 42 | # 图片加载库 43 | cached_network_image: ^3.2.1 44 | # Toast库 45 | fluttertoast: ^8.0.9 46 | # 屏幕适配工具 47 | flutter_screenutil: ^5.5.3+2 48 | # Flutter WebView 49 | flutter_inappwebview: ^5.4.3+7 50 | # 数据共享 51 | provider: ^6.0.3 52 | #刷新库 53 | easy_refresh: ^3.0.4+1 54 | #解析html 55 | flutter_html: ^2.2.1 56 | #网络请求cookie 57 | cookie_jar: ^3.0.1 58 | dio_cookie_manager: ^2.0.0 59 | #国际化 60 | intl: ^0.17.0 61 | #生命周期 62 | flutter_lifecycle_aware: ^0.0.2 63 | 64 | # 开发依赖 65 | dev_dependencies: 66 | # 创建项目自带 67 | flutter_test: 68 | sdk: flutter 69 | # 创建项目自带 70 | flutter_lints: ^2.0.0 71 | 72 | 73 | 74 | # The following section is specific to Flutter packages. 75 | flutter: 76 | 77 | # 创建项目自带 78 | uses-material-design: true 79 | 80 | # 资源路径配置 81 | assets: 82 | - assets/images/ 83 | - assets/html/ 84 | 85 | # 本地化 86 | flutter_intl: 87 | enabled: true 88 | -------------------------------------------------------------------------------- /lib/common/global_value.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/utils/shared_preferences_utils.dart'; 2 | 3 | class GlobalValue { 4 | ///用户信息 5 | static const String spUid = "spUid"; 6 | static const String spUser = "spUser"; 7 | 8 | ///本地化 9 | static const String spLocal = "spLocal"; 10 | 11 | ///主题 12 | static const String spTheme = "spTheme"; 13 | 14 | ///暗黑模式 15 | static const String spDarkMode = "spDarkMode"; 16 | 17 | ///用户是否已经登录 18 | static Future isLogin() async { 19 | int? uid = await SpUtil.getInstance().getInt(spUid); 20 | return !(uid == null || uid <= 0); 21 | } 22 | 23 | ///设置登录状态 24 | ///uid==null || uid==0 未登录状态 25 | static void setLoginState(int uid) async { 26 | await SpUtil.getInstance().setInt(spUid, uid); 27 | } 28 | 29 | ///保存用户信息 30 | static void setUserJson(String userJson) async { 31 | await SpUtil.getInstance().setString(spUser, userJson); 32 | } 33 | 34 | ///获取用户信息 35 | static Future getUserJson() async { 36 | return await SpUtil.getInstance().getString(spUser); 37 | } 38 | 39 | ///存储local 40 | static Future setLocalIndex(int localIndex) async { 41 | return await SpUtil.getInstance().setInt(spLocal, localIndex); 42 | } 43 | 44 | ///获取local 45 | static Future getLocalIndex() async { 46 | return await SpUtil.getInstance().getInt(spLocal); 47 | } 48 | 49 | ///存储theme 50 | static Future setThemeIndex(int themeIndex) async { 51 | return await SpUtil.getInstance().setInt(spTheme, themeIndex); 52 | } 53 | 54 | ///获取theme 55 | static Future getThemeIndex() async { 56 | return await SpUtil.getInstance().getInt(spTheme); 57 | } 58 | 59 | ///存储暗黑模式 60 | static Future setDarkMode(bool darkModel) async { 61 | return await SpUtil.getInstance().setBool(spDarkMode, darkModel); 62 | } 63 | 64 | ///获取暗黑模式 65 | static Future getDarkMode() async { 66 | return await SpUtil.getInstance().getBool(spDarkMode); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/modules/search/model/search_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/search/model/search_dao.dart'; 2 | import 'package:flutter_wan_android/core/net/http_request.dart'; 3 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 4 | import 'package:flutter_wan_android/modules/search/model/search_entity.dart'; 5 | 6 | import '../../../core/net/cancel/http_canceler.dart'; 7 | import '../../../core/net/http_result.dart'; 8 | 9 | ///SearchModel 10 | class SearchModel { 11 | SearchDao dao = SearchDao(); 12 | 13 | void close() { 14 | dao.close(); 15 | } 16 | 17 | ///获取本地数据 18 | Future?> getLocalData() async { 19 | return await dao.query(); 20 | } 21 | 22 | ///删除本地数据 23 | Future deleteLocalData({int? id}) async { 24 | return dao.delete(id: id); 25 | } 26 | 27 | ///新增或者更新数据 28 | Future insertOrUpdateLocalData(String value, {int? id}) async { 29 | return await dao.insertOrUpdate(value, id: id); 30 | } 31 | 32 | ///热门搜索API 33 | String hotKeyApi = "/hotkey/json"; 34 | 35 | ///搜索内容API 36 | String searchApi = "/article/query/0/json"; 37 | 38 | ///获取服务器热门搜索词 39 | Future> getHotKeyFromServer() async { 40 | ///结果 41 | Map json = await HttpRequest.get(hotKeyApi); 42 | 43 | ///解析 44 | HttpResult result = HttpResult().convert(json); 45 | return result; 46 | } 47 | 48 | ///搜索内容 49 | Future> getContentFromServer( 50 | String key, HttpCanceler? canceler) async { 51 | Map? params = {"k": key}; 52 | 53 | ///结果 54 | Map json = 55 | await HttpRequest.post(searchApi, params: params, canceler: canceler); 56 | 57 | ///解析 58 | HttpResult result = 59 | HttpResult().convert(json); 60 | 61 | result.list = HttpResult.convertList(json['data']["datas"]); 62 | return result; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_route_observer.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:flutter_wan_android/modules/main/view_model/locale_view_model.dart'; 5 | import 'package:flutter_wan_android/modules/main/view_model/theme_view_model.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | import 'config/router_config.dart'; 9 | import 'generated/l10n.dart'; 10 | import 'modules/main/view/main_page.dart'; 11 | 12 | void main() { 13 | runApp(const MyApp()); 14 | } 15 | 16 | class MyApp extends StatelessWidget { 17 | const MyApp({Key? key}) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return MultiProvider( 22 | providers: [ 23 | ChangeNotifierProvider(create: (context) => LocaleViewModel()), 24 | ChangeNotifierProvider(create: (context) => ThemeViewModel()) 25 | ], 26 | child: Consumer2( 27 | builder: (context, themeModel, localModel, child) { 28 | return MaterialApp( 29 | debugShowCheckedModeBanner: false, 30 | 31 | /// widget本地化代理设置 32 | localizationsDelegates: const [ 33 | GlobalMaterialLocalizations.delegate, 34 | GlobalCupertinoLocalizations.delegate, 35 | GlobalWidgetsLocalizations.delegate, 36 | S.delegate //自定义国际化 37 | ], 38 | 39 | ///指定语言 40 | locale: localModel.locale, 41 | 42 | /// widget本地化支持语言 43 | supportedLocales: S.delegate.supportedLocales, 44 | 45 | /// 生命周期感知 46 | navigatorObservers: [LifecycleRouteObserver.routeObserver], 47 | 48 | ///路由表配置 49 | routes: RouterConfig.routes, 50 | 51 | title: 'WanAndroid', 52 | 53 | ///主题 54 | theme: themeModel.themeData, 55 | 56 | ///暗黑主题 57 | darkTheme: themeModel.themeData, 58 | home: const MainPage(), 59 | ); 60 | }, 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/modules/knowledge/model/knowledge_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 2 | import 'package:flutter_wan_android/core/net/http_request.dart'; 3 | import 'package:flutter_wan_android/core/net/http_result.dart'; 4 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 5 | import 'package:flutter_wan_android/modules/knowledge/model/nav_entity.dart'; 6 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 7 | 8 | class KnowledgeModel { 9 | ///体系列表 10 | final String systemListApi = "tree/json"; 11 | 12 | ///导航列表 13 | final String navListApi = "navi/json"; 14 | 15 | ///分类下文章列表 16 | final String categoryArticleListApi = "article/list/%1/json?cid=%2"; 17 | 18 | ///获取体系分类列表 19 | Future> getSystemList( 20 | HttpCanceler? canceler) async { 21 | ///结果 22 | Map json = 23 | await HttpRequest.get(systemListApi, canceler: canceler); 24 | 25 | ///解析 26 | HttpResult result = 27 | HttpResult().convert(json); 28 | 29 | return result; 30 | } 31 | 32 | ///获取导航分类列表 33 | Future> getNavList(HttpCanceler? canceler) async { 34 | ///结果 35 | Map json = 36 | await HttpRequest.get(navListApi, canceler: canceler); 37 | 38 | ///解析 39 | HttpResult result = HttpResult().convert(json); 40 | 41 | return result; 42 | } 43 | 44 | ///获取项目列表 45 | ///projectId:项目分类ID 46 | Future> getCategoryArticleList( 47 | int projectId, int pageIndex, HttpCanceler canceler) async { 48 | String api = categoryArticleListApi.replaceAll('%1', pageIndex.toString()); 49 | api = api.replaceAll('%2', projectId.toString()); 50 | 51 | ///结果 52 | Map json = await HttpRequest.get(api, canceler: canceler); 53 | 54 | ///解析 55 | HttpResult result = 56 | HttpResult().convert(json); 57 | 58 | result.list = HttpResult.convertList(json['data']["datas"]); 59 | 60 | return result; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/modules/home/model/home_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/core/net/http_result.dart'; 2 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 3 | import 'package:flutter_wan_android/modules/home/model/banner_entity.dart'; 4 | 5 | import '../../../core/net/cancel/http_canceler.dart'; 6 | import '../../../core/net/http_request.dart'; 7 | 8 | class HomeModel { 9 | ///文章列表 10 | final String articleListApi = "article/list/%1/json"; 11 | 12 | ///置顶文章列表 13 | final String articleTopListApi = "article/top/json"; 14 | 15 | ///轮播图 16 | final String bannerListApi = "banner/json"; 17 | 18 | ///获取文章列表 19 | Future> getArticleList( 20 | int pageIndex, HttpCanceler? canceler) async { 21 | ///参数 22 | String api = articleListApi.replaceAll("%1", pageIndex.toString()); 23 | 24 | ///结果 25 | Map json = await HttpRequest.get(api, canceler: canceler); 26 | 27 | ///解析 28 | HttpResult result = 29 | HttpResult().convert(json); 30 | 31 | result.list = HttpResult.convertList(json['data']["datas"]); 32 | 33 | return result; 34 | } 35 | 36 | ///获取轮播图列表 37 | Future> getBannerList(HttpCanceler? canceler) async { 38 | ///结果 39 | Map json = 40 | await HttpRequest.get(bannerListApi, canceler: canceler); 41 | 42 | ///解析 43 | HttpResult result = HttpResult().convert(json); 44 | 45 | return result; 46 | } 47 | 48 | ///获取置顶文章列表 49 | Future> getArticleTopList( 50 | HttpCanceler? canceler) async { 51 | ///结果 52 | Map json = 53 | await HttpRequest.get(articleTopListApi, canceler: canceler); 54 | 55 | ///解析 56 | HttpResult result = 57 | HttpResult().convert(json); 58 | 59 | List list = result.list!; 60 | ArticleEntity entity; 61 | for (int i = 0; i < list.length; i++) { 62 | entity = list[i]; 63 | entity.isTop = true; //置顶 64 | list[i] = entity; 65 | } 66 | 67 | result.list = list; 68 | 69 | return result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/utils/shared_preferences_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | /// shared_preferences 工具类 4 | /// 封装一层,方面后续维护 5 | class SpUtil { 6 | /// 静态变量指向自身 7 | static final SpUtil _instance = SpUtil._(); 8 | 9 | /// 私有构造器 10 | SpUtil._(); 11 | 12 | /// 方案1:静态方法获得实例变量 13 | static SpUtil getInstance() => _instance; 14 | 15 | /// 方案2:工厂构造方法获得实例变量(不能异步) 16 | //factory SPUtils() => _instance; 17 | 18 | /// 方案3:静态属性获得实例变量 19 | //static SPUtils get instance => _instance; 20 | 21 | Future setString(String key, String value) async { 22 | final prefs = await SharedPreferences.getInstance(); 23 | return prefs.setString(key, value); 24 | } 25 | 26 | Future getString(String key) async { 27 | final prefs = await SharedPreferences.getInstance(); 28 | return prefs.getString(key); 29 | } 30 | 31 | Future setInt(String key, int value) async { 32 | final prefs = await SharedPreferences.getInstance(); 33 | return prefs.setInt(key, value); 34 | } 35 | 36 | Future getInt(String key) async { 37 | final prefs = await SharedPreferences.getInstance(); 38 | return prefs.getInt(key); 39 | } 40 | 41 | Future setBool(String key, bool value) async { 42 | final prefs = await SharedPreferences.getInstance(); 43 | return prefs.setBool(key, value); 44 | } 45 | 46 | Future getBool(String key) async { 47 | final prefs = await SharedPreferences.getInstance(); 48 | return prefs.getBool(key); 49 | } 50 | 51 | Future setDouble(String key, double value) async { 52 | final prefs = await SharedPreferences.getInstance(); 53 | return prefs.setDouble(key, value); 54 | } 55 | 56 | Future getDouble(String key) async { 57 | final prefs = await SharedPreferences.getInstance(); 58 | return prefs.getDouble(key); 59 | } 60 | 61 | Future setStringList(String key, List value) async { 62 | final prefs = await SharedPreferences.getInstance(); 63 | return prefs.setStringList(key, value); 64 | } 65 | 66 | Future?> getStringList(String key) async { 67 | final prefs = await SharedPreferences.getInstance(); 68 | return prefs.getStringList(key); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/core/net/convert/json_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 3 | 4 | /// 5 | /// 数据转换逻辑参考 generated > json > base > json_convert_content.dart 6 | /// 7 | /// 在没有实现动态处理 _convertFuncMap 和 _getListChildType 之前,直接使用该文件的功能 8 | /// 9 | 10 | /// json 数据转换实现原理 11 | /// 12 | /// 1.手动编写实体类,并编写数据转换逻辑 13 | /// 借助官方 dart:convert库,通过 json.decode() 方法将 string 类型的数据转为 Map 数据结构:Map,再通过 map[key] 取数据 14 | /*class Ip { 15 | String origin; 16 | 17 | Ip(this.origin); 18 | 19 | Ip.fromJson(Map json) : origin = json['origin']; 20 | 21 | Map toJson() => { 22 | "origin": origin, 23 | }; 24 | }*/ 25 | 26 | /// 27 | /// 2.json_serializable 是dart官方推荐和提供的JSON转Model的方式 28 | /// 2.1添加依赖 29 | /// dependencies: 30 | /// json_annotation: ^4.4.0 31 | /// 32 | /// dev_dependencies: 33 | /// build_runner: ^2.0.0 34 | /// json_serializable: ^6.0.0 35 | /// 36 | /// 2.2 添加 model 37 | 38 | /*part 'user.g.dart'; ///自动生成,但是需要先手动引入 39 | 40 | @JsonSerializable() 41 | class User { 42 | String name; 43 | String email; 44 | @JsonKey(name: "register_date") 45 | String registerDate; 46 | List courses; 47 | Computer computer; 48 | 49 | User(this.name, this.email, this.registerDate, this.courses, this.computer); 50 | 51 | factory User.fromJson(Map json) => _$UserFromJson(json); 52 | Map toJson() => _$UserToJson(this); 53 | 54 | @override 55 | String toString() { 56 | return 'User{name: $name, email: $email, registerDate: $registerDate, courses: $courses, computer: $computer}'; 57 | } 58 | 59 | } 60 | 61 | part 'computer.g.dart'; 62 | 63 | @JsonSerializable() 64 | class Computer { 65 | String brand; 66 | double price; 67 | 68 | Computer(this.brand, this.price); 69 | 70 | factory Computer.fromJson(Map json) => _$ComputerFromJson(json); 71 | Map toJson() => _$ComputerToJson(this); 72 | 73 | @override 74 | String toString() { 75 | return 'Computer{brand: $brand, price: $price}'; 76 | } 77 | }*/ 78 | 79 | /// 80 | /// 81 | /// 2.3 生成代码:flutter pub run build_runner build 82 | /// 83 | /// 84 | /// 3. 编辑器插件 FlutterJsonBeanFactory 85 | /// 86 | /// 87 | -------------------------------------------------------------------------------- /lib/modules/account/model/account_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_wan_android/modules/account/model/user_entity.dart'; 4 | 5 | import '../../../common/global_value.dart'; 6 | import '../../../core/net/cancel/http_canceler.dart'; 7 | import '../../../core/net/http_request.dart'; 8 | import '../../../core/net/http_result.dart'; 9 | 10 | class AccountModel { 11 | ///登录 12 | String loginApi = "user/login"; 13 | 14 | ///注册 15 | String registerApi = "user/register"; 16 | 17 | ///退出登录 18 | String logoutApi = "user/logout/json"; 19 | 20 | ///登录 21 | Future> login( 22 | String account, String psw, HttpCanceler? canceler) async { 23 | ///参数 24 | Map? params = {"username": account, "password": psw}; 25 | 26 | ///结果 27 | Map value = 28 | await HttpRequest.post(loginApi, params: params, canceler: canceler); 29 | 30 | return HttpResult().convert(value); 31 | } 32 | 33 | ///注册 34 | Future> register(String account, String psw, 35 | String confirmPsw, HttpCanceler? canceler) async { 36 | ///参数 37 | Map? params = { 38 | "username": account, 39 | "password": psw, 40 | "repassword": confirmPsw 41 | }; 42 | 43 | ///结果 44 | Map value = 45 | await HttpRequest.post(registerApi, params: params, canceler: canceler); 46 | 47 | return HttpResult().convert(value); 48 | } 49 | 50 | ///退出登录 51 | Future logout({HttpCanceler? canceler}) async { 52 | ///结果 53 | Map value = 54 | await HttpRequest.get(logoutApi, canceler: canceler); 55 | 56 | return HttpResult().convert(value); 57 | } 58 | 59 | ///保存登录数据 60 | void saveUser(UserEntity entity) { 61 | GlobalValue.setLoginState(entity.uid ?? 0); 62 | GlobalValue.setUserJson(entity.toString()); 63 | } 64 | 65 | ///获取账号信息 66 | Future getUser() async { 67 | String? jsonStr = await GlobalValue.getUserJson(); 68 | dynamic jsonMap = jsonDecode(jsonStr ?? "{}"); 69 | return UserEntity.fromJson(jsonMap); 70 | } 71 | 72 | ///是否已经登录 73 | Future isLogin() async { 74 | return await GlobalValue.isLogin(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/helper/db_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/book/model/study_dao.dart'; 2 | import 'package:flutter_wan_android/modules/search/model/search_dao.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | ///数据库辅助类 6 | class SqliteHelper { 7 | Database? _database; 8 | 9 | ///数据库路径/名称 10 | final String path = "zt_com_flutter.db"; 11 | 12 | ///数据库版本 13 | final int version = 1; 14 | 15 | ///打开数据库 16 | Future get database async { 17 | _database ??= await openDatabase( 18 | path, version: version, 19 | 20 | ///数据库创建 21 | onCreate: (Database db, int version) { 22 | SearchDao.createTable(db); 23 | StudyDao.createTable(db); 24 | }, 25 | 26 | ///数据库升级 27 | onUpgrade: (Database db, int oldVersion, int newVersion) { 28 | switch (newVersion) { 29 | case 2: 30 | break; 31 | } 32 | }, 33 | ); 34 | 35 | return _database!; 36 | } 37 | 38 | ///关闭数据库 39 | void close() async { 40 | (await database).close(); 41 | } 42 | 43 | ///插入数据 44 | Future insert(String table, Map values) async { 45 | return await (await database).insert(table, values); 46 | } 47 | 48 | ///删除数据 49 | Future delete(String table, 50 | {String? where, List? whereArgs}) async { 51 | return await (await database) 52 | .delete(table, where: where, whereArgs: whereArgs); 53 | } 54 | 55 | ///更新数据 56 | Future update(String table, Map values, 57 | {String? where, List? whereArgs}) async { 58 | return (await database) 59 | .update(table, values, where: where, whereArgs: whereArgs); 60 | } 61 | 62 | ///查询数据 63 | Future>?> query(String table, 64 | {List? columns, 65 | String? where, 66 | List? whereArgs, 67 | String? groupBy, 68 | String? having, 69 | String? orderBy, 70 | int? limit, 71 | int? offset}) async { 72 | List> list = await (await database).query(table, 73 | columns: columns, 74 | where: where, 75 | whereArgs: whereArgs, 76 | groupBy: groupBy, 77 | having: having, 78 | orderBy: orderBy, 79 | limit: limit, 80 | offset: offset); 81 | 82 | return list; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/modules/search/model/search_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/base/abs_dao.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | import 'search_entity.dart'; 5 | 6 | ///SearchDao 7 | class SearchDao extends AbsDao { 8 | ///搜索表 9 | static const String tbSearch = "tb_search"; 10 | 11 | ///搜索ID 12 | static const String fSearchId = "f_search_id"; 13 | 14 | ///搜索值 15 | static const String fSearchValue = "f_search_value"; 16 | 17 | ///搜索时间 18 | static const String fSearchTime = "f_search_time"; 19 | 20 | ///创建表 21 | static void createTable(Database db) { 22 | ///创建搜索表 23 | String sql = ''' 24 | create table $tbSearch( 25 | $fSearchId integer primary key autoincrement, 26 | $fSearchValue text not null, 27 | $fSearchTime integer not null) 28 | '''; 29 | 30 | db.execute(sql); 31 | } 32 | 33 | Future insertOrUpdate(String value, {int? id}) async { 34 | SearchEntity entity = 35 | SearchEntity(value: value, time: DateTime.now().millisecondsSinceEpoch); 36 | if (id == null) { 37 | entity.id = await helper.insert(tbSearch, toMap(entity)); 38 | } else { 39 | await helper.update(tbSearch, toMap(entity), 40 | where: "$fSearchId=?", whereArgs: [id]); 41 | } 42 | return entity; 43 | } 44 | 45 | Future delete({int? id}) async { 46 | return await helper.delete(tbSearch, 47 | where: id == null ? null : "$fSearchId=?", 48 | whereArgs: id == null ? null : [id]); 49 | } 50 | 51 | Future?> query() async { 52 | List>? list = 53 | await helper.query(tbSearch, orderBy: "$fSearchTime DESC"); 54 | List entityList = []; 55 | if (list != null) { 56 | for (var item in list) { 57 | entityList.add(fromMap(item)); 58 | } 59 | } 60 | 61 | return entityList; 62 | } 63 | 64 | SearchEntity fromMap(Map map) { 65 | int id = map[fSearchId] as int; 66 | String value = map[fSearchValue] as String; 67 | int time = map[fSearchTime] as int; 68 | return SearchEntity(value: value, time: time, id: id); 69 | } 70 | 71 | Map toMap(SearchEntity entity) { 72 | var map = { 73 | fSearchValue: entity.value, 74 | fSearchTime: entity.time 75 | }; 76 | if (entity.id != null) { 77 | map[fSearchId] = entity.id; 78 | } 79 | return map; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.zt.flutter_wan_android" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:intl/message_lookup_by_library.dart'; 16 | import 'package:intl/src/intl_helpers.dart'; 17 | 18 | import 'messages_en.dart' as messages_en; 19 | import 'messages_zh.dart' as messages_zh; 20 | 21 | typedef Future LibraryLoader(); 22 | Map _deferredLibraries = { 23 | 'en': () => new Future.value(null), 24 | 'zh': () => new Future.value(null), 25 | }; 26 | 27 | MessageLookupByLibrary? _findExact(String localeName) { 28 | switch (localeName) { 29 | case 'en': 30 | return messages_en.messages; 31 | case 'zh': 32 | return messages_zh.messages; 33 | default: 34 | return null; 35 | } 36 | } 37 | 38 | /// User programs should call this before using [localeName] for messages. 39 | Future initializeMessages(String localeName) async { 40 | var availableLocale = Intl.verifiedLocale( 41 | localeName, (locale) => _deferredLibraries[locale] != null, 42 | onFailure: (_) => null); 43 | if (availableLocale == null) { 44 | return new Future.value(false); 45 | } 46 | var lib = _deferredLibraries[availableLocale]; 47 | await (lib == null ? new Future.value(false) : lib()); 48 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 49 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 50 | return new Future.value(true); 51 | } 52 | 53 | bool _messagesExistFor(String locale) { 54 | try { 55 | return _findExact(locale) != null; 56 | } catch (e) { 57 | return false; 58 | } 59 | } 60 | 61 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { 62 | var actualLocale = 63 | Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); 64 | if (actualLocale == null) return null; 65 | return _findExact(actualLocale); 66 | } 67 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/modules/account/widget/login_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ///登录输入框 4 | class LoginTextField extends StatefulWidget { 5 | String hintText; 6 | IconData prefixIcon; 7 | IconData suffixIcon; 8 | bool obscureText; 9 | TextEditingController controller; 10 | TextInputAction textInputAction; 11 | VoidCallback onSuffixPressed; 12 | 13 | FocusNode? focusNode; 14 | ValueChanged? onSubmitted; 15 | ValueChanged? onChanged; 16 | 17 | LoginTextField( 18 | {Key? key, 19 | required this.hintText, 20 | required this.prefixIcon, 21 | required this.suffixIcon, 22 | required this.obscureText, 23 | required this.controller, 24 | required this.textInputAction, 25 | required this.onSuffixPressed, 26 | this.focusNode, 27 | this.onSubmitted, 28 | this.onChanged}) 29 | : super(key: key); 30 | 31 | @override 32 | State createState() => _LoginTextFieldState(); 33 | } 34 | 35 | class _LoginTextFieldState extends State { 36 | @override 37 | Widget build(BuildContext context) { 38 | return Container( 39 | margin: const EdgeInsets.only(top: 10), 40 | child: TextField( 41 | style: TextStyle(fontSize: 16, color: Colors.grey[900]), 42 | controller: widget.controller, 43 | onChanged: widget.onChanged, 44 | 45 | ///输入框装饰器 46 | decoration: InputDecoration( 47 | hintStyle: Theme.of(context).textTheme.bodyMedium, 48 | hintText: widget.hintText, 49 | //前缀图标 50 | prefixIcon: Icon( 51 | widget.prefixIcon, 52 | size: 28, 53 | color: Theme.of(context).primaryColor, 54 | ), 55 | //后缀图标 56 | suffix: IconButton( 57 | onPressed: widget.onSuffixPressed, 58 | icon: Icon(widget.suffixIcon, size: 24), 59 | ), 60 | //默认边框装饰 61 | enabledBorder: UnderlineInputBorder( 62 | borderSide: BorderSide(color: Colors.grey[300]!)), 63 | //获取焦点边框装饰 64 | focusedBorder: UnderlineInputBorder( 65 | borderSide: BorderSide(color: Theme.of(context).primaryColor)), 66 | ), 67 | cursorColor: Theme.of(context).primaryColor, 68 | //密码模式 69 | obscureText: widget.obscureText, 70 | //键盘完成按钮样式 71 | textInputAction: widget.textInputAction, 72 | //完成按钮事件 73 | onSubmitted: widget.onSubmitted, 74 | focusNode: widget.focusNode, 75 | ), 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/modules/book/view/main_book.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle.dart'; 3 | import 'package:flutter_wan_android/config/router_config.dart'; 4 | import 'package:flutter_wan_android/helper/image_helper.dart'; 5 | import 'package:flutter_wan_android/helper/router_helper.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | import '../../../generated/l10n.dart'; 9 | import '../../book/model/book_entity.dart'; 10 | import '../view_model/book_view_model.dart'; 11 | 12 | class MainBookPage extends StatefulWidget { 13 | const MainBookPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | State createState() => _MainBookPageState(); 17 | } 18 | 19 | class _MainBookPageState extends State 20 | with AutomaticKeepAliveClientMixin, Lifecycle { 21 | final BookViewModel bookViewModel = BookViewModel(); 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | getLifecycle().addObserver(bookViewModel); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | super.build(context); 32 | return ChangeNotifierProvider( 33 | create: (_) => bookViewModel, 34 | builder: (context, child) { 35 | return Scaffold( 36 | appBar: AppBar( 37 | title: Text(S.of(context).tab_book_course), 38 | ), 39 | body: bodyContent()); 40 | }, 41 | ); 42 | } 43 | 44 | void actionItemClick(BookEntity book) { 45 | RouterHelper.pushNamed(context, RouterConfig.bookDetailsPage, 46 | arguments: book); 47 | } 48 | 49 | Widget bodyContent() { 50 | return Consumer(builder: (context, viewModel, child) { 51 | return GridView.builder( 52 | padding: const EdgeInsets.all(15), 53 | itemCount: viewModel.dataArray.length, 54 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 55 | crossAxisCount: 2, 56 | crossAxisSpacing: 15, 57 | mainAxisSpacing: 15, 58 | childAspectRatio: 0.7), 59 | itemBuilder: (context, index) { 60 | return itemWidget(viewModel.dataArray[index]); 61 | }); 62 | }); 63 | } 64 | 65 | Widget itemWidget(BookEntity book) { 66 | return GestureDetector( 67 | child: ClipRRect( 68 | borderRadius: BorderRadius.circular(4), 69 | child: ImageHelper.network(book.cover, fit: BoxFit.cover)), 70 | onTap: () => actionItemClick(book), 71 | ); 72 | } 73 | 74 | @override 75 | bool get wantKeepAlive => true; 76 | } 77 | -------------------------------------------------------------------------------- /lib/helper/image_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | ///Image辅助类 7 | class ImageHelper { 8 | ///测试用的图片链接 9 | static List imageUrl = [ 10 | "https://img2.baidu.com/it/u=1994380678,3283034272&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657213200&t=d57830e0ca280cc0f87fdbf10b25305b", 11 | "https://img2.baidu.com/it/u=2860188096,638334621&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657213200&t=cc435e450717a2beb0623dd45752f75f", 12 | "https://img1.baidu.com/it/u=592570905,1313515675&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657213200&t=1d3fe5d6db1996aa3b45c8636347869d", 13 | "https://img2.baidu.com/it/u=4244269751,4000533845&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657213200&t=9e3bbec87e572ee9bf269a018c71e0ac", 14 | "https://img1.baidu.com/it/u=2029513305,2137933177&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657213200&t=fc9d00fc14a8feeb19be958ba428ecba", 15 | "https://img0.baidu.com/it/u=1694074520,2517635995&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1657472400&t=3b8cee3f0f6a844e69f3b43dff3d8465" 16 | ]; 17 | 18 | ///随机URL 19 | static String randomUrl() { 20 | return imageUrl[Random().nextInt(imageUrl.length)]; 21 | } 22 | 23 | ///assets路径转换 24 | static String wrapAssets(String url) { 25 | return "assets/images/$url"; 26 | } 27 | 28 | /// 加载网络图片(带缓存) 29 | /// imageUrl:图片地址 30 | /// placeholder:加载中占位符 31 | /// errorWidget:加载失败占位符 32 | static Widget network(String imageUrl, 33 | {double? height, 34 | double? width, 35 | BoxFit? fit, 36 | Widget? placeholder, 37 | Widget? error}) { 38 | ///默认值 39 | Widget placeholderWidget = placeholder ??= Stack( 40 | alignment: Alignment.center, 41 | children: const [ 42 | SizedBox(height: 30, width: 30, child: CircularProgressIndicator()) 43 | ], 44 | ); 45 | 46 | Widget errorWidget = error ??= const Icon(Icons.error); 47 | 48 | try { 49 | return CachedNetworkImage( 50 | imageUrl: imageUrl, 51 | placeholder: (context, url) => placeholderWidget, 52 | errorWidget: (context, url, error) => errorWidget, 53 | height: height, 54 | width: width, 55 | fit: fit, 56 | ); 57 | } catch (e) { 58 | return errorWidget; 59 | } 60 | } 61 | 62 | /// 加载 assets 图片 63 | /// url资源名称 64 | static Widget assets(String name, 65 | {double? height, double? width, BoxFit? fit, Color? color}) { 66 | return Image.asset( 67 | ImageHelper.wrapAssets(name), 68 | height: height, 69 | width: width, 70 | color: color, 71 | fit: fit, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/modules/me/view_model/me_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | import 'package:flutter_wan_android/modules/account/model/user_entity.dart'; 6 | import 'package:flutter_wan_android/modules/main/view_model/locale_view_model.dart'; 7 | import 'package:flutter_wan_android/modules/main/view_model/theme_view_model.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | import '../../account/model/account_model.dart'; 11 | 12 | /// me页面的ViewModel 13 | class MeViewModel extends ChangeNotifier with LifecycleObserver { 14 | late AccountModel model; 15 | 16 | MeViewModel() { 17 | model = AccountModel(); 18 | } 19 | 20 | ///用户信息实体类 21 | UserEntity _userEntity = UserEntity(); 22 | 23 | UserEntity get userEntity => _userEntity; 24 | 25 | set userEntity(UserEntity value) { 26 | _userEntity = value; 27 | notifyListeners(); 28 | } 29 | 30 | ///是否已经登录 31 | bool _isLogin = false; 32 | 33 | bool get isLogin => _isLogin; 34 | 35 | set isLogin(bool value) { 36 | _isLogin = value; 37 | notifyListeners(); 38 | } 39 | 40 | ///多语言下标 41 | int _localIndexValue = 0; 42 | 43 | int get localIndexValue => _localIndexValue; 44 | 45 | set localIndexValue(int value) { 46 | _localIndexValue = value; 47 | notifyListeners(); 48 | } 49 | 50 | ///主题下标 51 | int _themeIndex = ThemeViewModel.defaultThemeIndex; 52 | 53 | int get themeIndex => _themeIndex; 54 | 55 | set themeIndex(int value) { 56 | _themeIndex = value; 57 | notifyListeners(); 58 | } 59 | 60 | ///是否暗黑模式 61 | bool _darkMode = false; 62 | 63 | bool get darkMode => _darkMode; 64 | 65 | set darkMode(bool value) { 66 | _darkMode = value; 67 | notifyListeners(); 68 | } 69 | 70 | ///初始化用户信息 71 | void initUserData() async { 72 | isLogin = await model.isLogin(); 73 | if (isLogin) { 74 | userEntity = await model.getUser(); 75 | } 76 | } 77 | 78 | ///初始化本地化信息 79 | void initLocalData(BuildContext context) { 80 | localIndexValue = context.read().localIndex; 81 | } 82 | 83 | ///初始化主题信息 84 | void initThemeData(BuildContext context) { 85 | ThemeViewModel viewModel = context.read(); 86 | themeIndex = viewModel.themeIndex; 87 | darkMode = viewModel.darkMode; 88 | } 89 | 90 | @override 91 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 92 | if (state == LifecycleState.onResume) { 93 | /// 首帧绘制完成 94 | /// 初始化数据 95 | initLocalData(owner.getStateful().context); 96 | initThemeData(owner.getStateful().context); 97 | initUserData(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 使用 flutter 实现的 《玩Android》客户端 2 | 3 | ### 功能效果图 4 | | | | | 5 | | --- | --- | --- | 6 | | | | | 7 | | | | | 8 | | | | | 9 | | | | | 10 | 11 | ### 项目结构 12 | ``` 13 | |--lib 14 | |-- base (基类) 15 | |-- common (常用类,例如常量) 16 | |-- config (配置信息) 17 | |-- core (核心代码,例如:网络) 18 | |-- helper (功能辅助类) 19 | |-- I10n (国际化文本) 20 | |-- modules (业务模块:账户模块,文章模块,搜索模块,收藏模块,,,) 21 | |-- utils (工具类) 22 | |-- widget (全局通用控件) 23 | ``` 24 | 25 | ### 业务模块结构 26 | ``` 27 | |--modules 28 | |-- article (文章模块) 29 | |-- search (搜索模块) 30 | |-- collect (收藏模块) 31 | |-- ...... (其他模块) 32 | |-- account (账户模块) 33 | |-- model (数据管理model,实体类entity) 34 | |-- account_model (登录,注册,本地数据) 35 | |-- user_entity 36 | |-- view 37 | |-- login_page 38 | |-- register_page 39 | |-- view_model 40 | |-- login_view_model 41 | |-- register_view_model 42 | ``` 43 | 44 | 45 | ### 项目简介 46 | 47 | 大体上通过业务模块分包,使用 MVVM (基于 Provider )模式架构,目标是构建一个扩展性良好的应用,View 模块编写 widget 相关的布局信息,界面业务逻辑在 ViewModel 中实现,数据通过 Model 管理。 48 | 49 | 50 | ### 技术概览 51 | 52 | - dio : 网络数据请求 53 | - sqflite : 本地数据库管理 54 | - shared_preferences : 本地配置信息管理 55 | - path_provider : 文件读写(存储路径) 56 | - cached_network_image : 网络图片加载/缓存 57 | - fluttertoast : Toast 58 | - flutter_inappwebview : WebView 59 | - provider : 数据共享/状态管理 60 | - flutter_html : html文本展示 61 | - cookie_jar/dio_cookie_manager : 网络请求cookie管理 62 | - intl : 国际化 63 | - flutter_lifecycle_aware : 生命周期管理 64 | - easy_refresh : 下拉刷新/上拉加载 65 | - FlutterJsonBeanFactory : json 转 实体类(androidStudio插件) 66 | - Flutter Intl : 多语言(androidStudio插件) 67 | 68 | 69 | 70 | 71 | ### 附加产物 72 | 73 | 开发过程中发现 flutter 生命周期存在使用使用上的不足,封装了一个生命周期相关的库 [flutter_lifecycle](https://github.com/RuffianZhong/flutter_lifecycle) 74 | 75 | flutter_lifecycle 可以使任何对象具备生命周期感知能力,结合网络请求,实现根据生命周期自动取消网络请求 76 | 77 | 78 | ### 后续 79 | 80 | 逐步完善一个复杂项目可能用到的技术(例如:组件化)或者 flutter 相关的技术(编写一系列demo展示控件使用,动画使用,自定义控件,等等) 81 | 82 | 83 | ### 鸣谢 84 | 85 | 项目数据接口来自 [玩Android](https://www.wanandroid.com/) 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/modules/search/view_model/search_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | import 'package:flutter_wan_android/core/net/cancel/zt_http_cancel.dart'; 6 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 7 | import 'package:flutter_wan_android/modules/search/model/search_entity.dart'; 8 | import 'package:flutter_wan_android/modules/search/model/search_model.dart'; 9 | 10 | ///SearchViewModel 11 | class SearchViewModel extends ChangeNotifier with LifecycleObserver { 12 | late HttpCanceler httpCanceler; 13 | SearchModel model = SearchModel(); 14 | 15 | ///编辑数据模式 16 | bool _editingData = false; 17 | 18 | bool get editingData => _editingData; 19 | 20 | set editingData(bool value) { 21 | _editingData = value; 22 | notifyListeners(); 23 | } 24 | 25 | ///服务器标签 26 | List? _serverLabels = []; 27 | 28 | List? get serverLabels => _serverLabels; 29 | 30 | set serverLabels(List? value) { 31 | _serverLabels = value; 32 | notifyListeners(); 33 | } 34 | 35 | void getHotKeyFromServer() async { 36 | model.getHotKeyFromServer().then((value) { 37 | if (value.success) { 38 | serverLabels = value.list; 39 | } 40 | }); 41 | } 42 | 43 | ///本地标签 44 | List? _localLabels = []; 45 | 46 | List? get localLabels => _localLabels; 47 | 48 | set localLabels(List? value) { 49 | _localLabels = value; 50 | notifyListeners(); 51 | } 52 | 53 | void getSearchKeyFromLocal() async { 54 | model.getLocalData().then((value) => localLabels = value); 55 | } 56 | 57 | ///展示搜索相关界面 58 | bool _showSearchUI = true; 59 | 60 | bool get showSearchUI => _showSearchUI; 61 | 62 | set showSearchUI(bool value) { 63 | _showSearchUI = value; 64 | notifyListeners(); 65 | } 66 | 67 | ///文章列表 68 | List _articleList = []; 69 | 70 | List get articleList => _articleList; 71 | 72 | set articleList(List value) { 73 | _articleList = value; 74 | notifyListeners(); 75 | } 76 | 77 | ///搜索内容 78 | void getContentFromServer(String key) { 79 | model.getContentFromServer(key, httpCanceler).then((value) { 80 | if (value.success) { 81 | articleList = value.list!; 82 | } 83 | }); 84 | } 85 | 86 | @override 87 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 88 | if (state == LifecycleState.onInit) { 89 | httpCanceler = HttpCanceler(owner); 90 | } else if (state == LifecycleState.onCreate) { 91 | ///初始化本地数据 92 | getSearchKeyFromLocal(); 93 | 94 | ///初始化服务器数据 95 | getHotKeyFromServer(); 96 | } else if (state == LifecycleState.onDestroy) { 97 | model.close(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/modules/home/view_model/home_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | import 'package:flutter_wan_android/core/net/http_result.dart'; 6 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 7 | import 'package:flutter_wan_android/modules/home/model/banner_entity.dart'; 8 | 9 | import '../../../core/net/cancel/http_canceler.dart'; 10 | import '../model/home_model.dart'; 11 | 12 | class HomeViewModel extends ChangeNotifier with LifecycleObserver { 13 | ///HttpCanceler 14 | late HttpCanceler canceler; 15 | 16 | ///model 17 | late HomeModel model; 18 | 19 | HomeViewModel() { 20 | model = HomeModel(); 21 | } 22 | 23 | ///快速回到顶部 24 | bool _quickToTop = false; 25 | 26 | bool get quickToTop => _quickToTop; 27 | 28 | set quickToTop(bool value) { 29 | _quickToTop = value; 30 | notifyListeners(); 31 | } 32 | 33 | ///数据页面下标 34 | int pageIndex = 0; 35 | 36 | ///文章列表 37 | List _articleList = []; 38 | 39 | List get articleList => _articleList; 40 | 41 | set articleList(List value) { 42 | _articleList = value; 43 | notifyListeners(); 44 | } 45 | 46 | ///置顶文章列表 47 | List _articleTopList = []; 48 | 49 | List get articleTopList => _articleTopList; 50 | 51 | set articleTopList(List value) { 52 | _articleTopList = value; 53 | notifyListeners(); 54 | } 55 | 56 | ///轮播图列表 57 | List _bannerList = []; 58 | 59 | List get bannerList => _bannerList; 60 | 61 | set bannerList(List value) { 62 | _bannerList = value; 63 | notifyListeners(); 64 | } 65 | 66 | ///获取内容列表 67 | Future> getArticleList(bool refresh) async { 68 | ///下拉刷新,下标从0开始 69 | if (refresh) pageIndex = 0; 70 | HttpResult result = 71 | await model.getArticleList(pageIndex, canceler); 72 | if (result.success) { 73 | if (refresh) articleList.clear(); 74 | articleList.addAll(result.list!); 75 | articleList = articleList; 76 | pageIndex++; 77 | } 78 | return result; 79 | } 80 | 81 | ///置顶文章 82 | Future> getArticleTopList( 83 | bool refresh, HttpCanceler canceler) async { 84 | HttpResult result = await model.getArticleTopList(canceler); 85 | if (result.success) { 86 | articleTopList = result.list ?? []; 87 | } 88 | return result; 89 | } 90 | 91 | @override 92 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 93 | if (state == LifecycleState.onInit) { 94 | canceler = HttpCanceler(owner); 95 | } else if (state == LifecycleState.onCreate) { 96 | /// 首帧绘制完成 97 | /// 初始化数据 98 | getArticleList(true); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/utils/screen_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui show window; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Screen Util. 6 | class ScreenUtil { 7 | double _screenWidth = 0.0; 8 | double _screenHeight = 0.0; 9 | double _screenDensity = 0.0; 10 | double _statusBarHeight = 0.0; 11 | double _bottomBarHeight = 0.0; 12 | double _appBarHeight = 0.0; 13 | double _navBarHeight = 0.0; 14 | MediaQueryData? _mediaQueryData; 15 | 16 | static final ScreenUtil _singleton = ScreenUtil(); 17 | 18 | static ScreenUtil get() { 19 | _singleton._init(); 20 | return _singleton; 21 | } 22 | 23 | _init() { 24 | MediaQueryData mediaQuery = MediaQueryData.fromWindow(ui.window); 25 | if (_mediaQueryData != mediaQuery) { 26 | _mediaQueryData = mediaQuery; 27 | _screenWidth = mediaQuery.size.width; 28 | _screenHeight = mediaQuery.size.height; 29 | _screenDensity = mediaQuery.devicePixelRatio; 30 | _statusBarHeight = mediaQuery.padding.top; 31 | _bottomBarHeight = mediaQuery.padding.bottom; 32 | _appBarHeight = kToolbarHeight; 33 | _navBarHeight = _appBarHeight + _statusBarHeight; 34 | } 35 | } 36 | 37 | /// screen width 38 | /// 屏幕 宽 39 | double get screenWidth => _screenWidth; 40 | 41 | /// screen height 42 | /// 屏幕 高 43 | double get screenHeight => _screenHeight; 44 | 45 | /// appBar height 46 | /// appBar 高 47 | double get appBarHeight => _appBarHeight; 48 | 49 | /// screen density 50 | /// 屏幕 像素密度 51 | double get screenDensity => _screenDensity; 52 | 53 | /// status bar Height 54 | /// 状态栏高度 55 | double get statusBarHeight => _statusBarHeight; 56 | 57 | /// bottom bar Height 58 | double get bottomBarHeight => _bottomBarHeight; 59 | 60 | /// 导航栏高度: 状态栏高度 + AppBar高度 61 | double get navBarHeight => _navBarHeight; 62 | 63 | /// media Query Data 64 | MediaQueryData? get mediaQueryData => _mediaQueryData; 65 | 66 | /// screen width 67 | /// 当前屏幕 宽 68 | static double getScreenW(BuildContext context) { 69 | MediaQueryData mediaQuery = MediaQuery.of(context); 70 | return mediaQuery.size.width; 71 | } 72 | 73 | /// screen height 74 | /// 当前屏幕 高 75 | static double getScreenH(BuildContext context) { 76 | MediaQueryData mediaQuery = MediaQuery.of(context); 77 | return mediaQuery.size.height; 78 | } 79 | 80 | /// screen density 81 | /// 当前屏幕 像素密度 82 | static double getScreenDensity(BuildContext context) { 83 | MediaQueryData mediaQuery = MediaQuery.of(context); 84 | return mediaQuery.devicePixelRatio; 85 | } 86 | 87 | /// status bar Height 88 | /// 当前状态栏高度 89 | static double getStatusBarH(BuildContext context) { 90 | MediaQueryData mediaQuery = MediaQuery.of(context); 91 | return mediaQuery.padding.top; 92 | } 93 | 94 | /// status bar Height 95 | /// 当前BottomBar高度 96 | static double getBottomBarH(BuildContext context) { 97 | MediaQueryData mediaQuery = MediaQuery.of(context); 98 | return mediaQuery.padding.bottom; 99 | } 100 | 101 | /// 当前MediaQueryData 102 | static MediaQueryData getMediaQueryData(BuildContext context) { 103 | MediaQueryData mediaQuery = MediaQuery.of(context); 104 | return mediaQuery; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/generated/json/article_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/generated/json/base/json_convert_content.dart'; 2 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 3 | import 'package:flutter_wan_android/helper/image_helper.dart'; 4 | 5 | import 'package:flutter_wan_android/modules/book/model/study_entity.dart'; 6 | 7 | import '../../../generated/json/base/json_convert_content.dart'; 8 | 9 | 10 | ArticleEntity $ArticleEntityFromJson(Map json) { 11 | final ArticleEntity articleEntity = ArticleEntity(); 12 | final int? id = jsonConvert.convert(json['id']); 13 | if (id != null) { 14 | articleEntity.id = id; 15 | } 16 | final String? userName = jsonConvert.convert(json['shareUser']); 17 | if (userName != null) { 18 | articleEntity.userName = userName; 19 | } 20 | final String? userIcon = jsonConvert.convert(json['userIcon']); 21 | if (userIcon != null) { 22 | articleEntity.userIcon = userIcon; 23 | } 24 | final String? link = jsonConvert.convert(json['link']); 25 | if (link != null) { 26 | articleEntity.link = link; 27 | } 28 | final String? title = jsonConvert.convert(json['title']); 29 | if (title != null) { 30 | articleEntity.title = title; 31 | } 32 | final String? date = jsonConvert.convert(json['niceDate']); 33 | if (date != null) { 34 | articleEntity.date = date; 35 | } 36 | final String? superChapterName = jsonConvert.convert(json['superChapterName']); 37 | if (superChapterName != null) { 38 | articleEntity.superChapterName = superChapterName; 39 | } 40 | final String? chapterName = jsonConvert.convert(json['chapterName']); 41 | if (chapterName != null) { 42 | articleEntity.chapterName = chapterName; 43 | } 44 | final int? chapterId = jsonConvert.convert(json['chapterId']); 45 | if (chapterId != null) { 46 | articleEntity.chapterId = chapterId; 47 | } 48 | final String? desc = jsonConvert.convert(json['desc']); 49 | if (desc != null) { 50 | articleEntity.desc = desc; 51 | } 52 | final String? cover = jsonConvert.convert(json['envelopePic']); 53 | if (cover != null) { 54 | articleEntity.cover = cover; 55 | } 56 | final bool? isTop = jsonConvert.convert(json['isTop']); 57 | if (isTop != null) { 58 | articleEntity.isTop = isTop; 59 | } 60 | final bool? collect = jsonConvert.convert(json['collect']); 61 | if (collect != null) { 62 | articleEntity.collect = collect; 63 | } 64 | final StudyEntity? study = jsonConvert.convert(json['study']); 65 | if (study != null) { 66 | articleEntity.study = study; 67 | } 68 | return articleEntity; 69 | } 70 | 71 | Map $ArticleEntityToJson(ArticleEntity entity) { 72 | final Map data = {}; 73 | data['id'] = entity.id; 74 | data['shareUser'] = entity.userName; 75 | data['userIcon'] = entity.userIcon; 76 | data['link'] = entity.link; 77 | data['title'] = entity.title; 78 | data['niceDate'] = entity.date; 79 | data['superChapterName'] = entity.superChapterName; 80 | data['chapterName'] = entity.chapterName; 81 | data['chapterId'] = entity.chapterId; 82 | data['desc'] = entity.desc; 83 | data['envelopePic'] = entity.cover; 84 | data['isTop'] = entity.isTop; 85 | data['collect'] = entity.collect; 86 | data['study'] = entity.study?.toJson(); 87 | return data; 88 | } -------------------------------------------------------------------------------- /lib/modules/book/view_model/book_details_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 5 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 6 | 7 | import '../../../core/net/cancel/http_canceler.dart'; 8 | import '../../../core/net/http_result.dart'; 9 | import '../model/book_model.dart'; 10 | import '../model/study_entity.dart'; 11 | 12 | class BookDetailsViewModel extends ChangeNotifier with LifecycleObserver { 13 | @override 14 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 15 | if (state == LifecycleState.onInit) { 16 | httpCanceler = HttpCanceler(owner); 17 | } else if (state == LifecycleState.onDestroy) { 18 | model.close(); 19 | } 20 | } 21 | 22 | late HttpCanceler httpCanceler; 23 | 24 | BookModel model = BookModel(); 25 | 26 | ///文章列表 27 | List _articleList = []; 28 | 29 | List get articleList => _articleList; 30 | 31 | set articleList(List value) { 32 | _articleList = value; 33 | notifyListeners(); 34 | } 35 | 36 | ///获取内容列表 37 | Future> getArticleList(int projectId) async { 38 | HttpResult result = 39 | await model.getBookArticleList(projectId, 0, httpCanceler); 40 | if (result.success) { 41 | _articleList.addAll(result.list!); 42 | } 43 | return result; 44 | } 45 | 46 | ///学习进度列表 47 | List _studyList = []; 48 | 49 | ///获取学习列表数据:本地数据库 50 | Future> getStudyListData(int bookId) async { 51 | _studyList = await model.dao.query(bookId); 52 | return _studyList; 53 | } 54 | 55 | ///初始化数据 56 | void initData(int projectId) { 57 | ///多个 future 58 | Iterable futures = [ 59 | getArticleList(projectId), 60 | getStudyListData(projectId) 61 | ]; 62 | 63 | ///等待多个 future 执行完成 64 | Future.wait(futures).then((value) { 65 | updateStudyData(_studyList, _articleList); 66 | }); 67 | } 68 | 69 | ///更新列表学习数据 70 | void updateStudyData( 71 | List studyList, List articleList) { 72 | if (studyList.isNotEmpty && articleList.isNotEmpty) { 73 | StudyEntity study; 74 | ArticleEntity article; 75 | 76 | ///遍历学习进度列表 77 | for (int i = 0; i < studyList.length; i++) { 78 | study = studyList[i]; 79 | 80 | ///遍历文章(章节列表) 81 | for (int j = 0; j < articleList.length; j++) { 82 | article = articleList[j]; 83 | 84 | ///匹配/更新学习进度 85 | if (article.id == study.articleId) { 86 | article.study = study; 87 | articleList[j] = article; 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | 94 | ///更新viewModel数据 95 | this.articleList = articleList; 96 | } 97 | 98 | ///插入或者更新数据 99 | Future insertOrUpdateStudy( 100 | int bookId, int articleId, double progress, 101 | {int? id}) async { 102 | StudyEntity study = 103 | await model.insertOrUpdateStudy(id: id, bookId, articleId, progress); 104 | 105 | updateStudyData([study], _articleList); 106 | 107 | return study; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/modules/book/model/study_dao.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_wan_android/modules/book/model/study_entity.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | import '../../../base/abs_dao.dart'; 5 | 6 | ///StudyDao 7 | class StudyDao extends AbsDao { 8 | ///表 9 | static const String tbBookStudy = "tb_book_study"; 10 | 11 | ///表ID 12 | static const String fBookStudyId = "f_book_study_id"; 13 | 14 | ///教程ID 15 | static const String fBookId = "f_book_id"; 16 | 17 | ///文章ID:章节ID 18 | static const String fArticleId = "f_article_id"; 19 | 20 | ///学习进度 21 | static const String fStudyProgress = "f_study_progress"; 22 | 23 | ///学习时间 24 | static const String fStudyTime = "f_study_time"; 25 | 26 | ///创建表 27 | static void createTable(Database db) { 28 | ///创建搜索表 29 | String sql = ''' 30 | create table $tbBookStudy( 31 | $fBookStudyId integer primary key autoincrement, 32 | $fBookId integer not null, 33 | $fArticleId integer not null, 34 | $fStudyProgress real not null, 35 | $fStudyTime integer not null) 36 | '''; 37 | 38 | db.execute(sql); 39 | } 40 | 41 | ///插入数据或者更新数据 42 | /// id ==null 插入数据 43 | Future insertOrUpdate( 44 | {int? id, 45 | required int bookId, 46 | required int articleId, 47 | required double progress}) async { 48 | StudyEntity entity = StudyEntity( 49 | id: id, 50 | bookId: bookId, 51 | articleId: articleId, 52 | progress: progress, 53 | time: DateTime.now().millisecondsSinceEpoch); 54 | 55 | if (id == null) { 56 | entity.id = await helper.insert(tbBookStudy, toMap(entity)); 57 | } else { 58 | await helper.update(tbBookStudy, toMap(entity), 59 | where: "$fBookStudyId=?", whereArgs: [id]); 60 | } 61 | return entity; 62 | } 63 | 64 | ///删除数据 65 | ///id == null 删除全部 66 | Future delete({int? id}) async { 67 | return await helper.delete(tbBookStudy, 68 | where: id == null ? null : "$fBookStudyId=?", 69 | whereArgs: id == null ? null : [id]); 70 | } 71 | 72 | ///按照书本 查询数据 73 | ///bookId:教程ID 74 | Future> query(int bookId) async { 75 | List>? list = await helper 76 | .query(tbBookStudy, where: "$fBookId=?", whereArgs: [bookId]); 77 | List entityList = []; 78 | if (list != null) { 79 | for (var item in list) { 80 | entityList.add(fromMap(item)); 81 | } 82 | } 83 | 84 | return entityList; 85 | } 86 | 87 | StudyEntity fromMap(Map map) { 88 | int id = map[fBookStudyId] as int; 89 | int bookId = map[fBookId] as int; 90 | int articleId = map[fArticleId] as int; 91 | double progress = map[fStudyProgress] as double; 92 | int time = map[fStudyTime] as int; 93 | 94 | return StudyEntity( 95 | id: id, 96 | bookId: bookId, 97 | articleId: articleId, 98 | progress: progress, 99 | time: time); 100 | } 101 | 102 | Map toMap(StudyEntity entity) { 103 | var map = { 104 | fBookId: entity.bookId, 105 | fArticleId: entity.articleId, 106 | fStudyProgress: entity.progress, 107 | fStudyTime: entity.time, 108 | }; 109 | if (entity.id != null) { 110 | map[fBookStudyId] = entity.id; 111 | } 112 | return map; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/modules/main/view_model/theme_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../common/global_value.dart'; 4 | 5 | ///主题ViewModel 6 | class ThemeViewModel extends ChangeNotifier { 7 | ///主题列表:直接使用 Colors.primaries 8 | static const List themeList = Colors.primaries; 9 | 10 | ///默认主题下标 11 | static const int defaultThemeIndex = 3; 12 | 13 | ///构造函数时获取本地缓存数据 14 | ThemeViewModel() { 15 | ///获取ThemeIndex 16 | GlobalValue.getThemeIndex().then((value) { 17 | themeIndex = value ?? defaultThemeIndex; 18 | themeColor = themeList[themeIndex]; 19 | }); 20 | 21 | ///获取是否暗黑模式 22 | GlobalValue.getDarkMode().then((value) { 23 | darkMode = value ?? false; 24 | }); 25 | } 26 | 27 | ///主题下标 28 | int _themeIndex = defaultThemeIndex; 29 | 30 | int get themeIndex => _themeIndex; 31 | 32 | set themeIndex(int value) { 33 | _themeIndex = value; 34 | notifyListeners(); 35 | } 36 | 37 | ///当前主题颜色 38 | MaterialColor _themeColor = themeList[7]; 39 | 40 | MaterialColor get themeColor => _themeColor; 41 | 42 | set themeColor(MaterialColor value) { 43 | _themeColor = value; 44 | notifyListeners(); 45 | } 46 | 47 | ///存储theme 48 | void setThemeIndex(int index) { 49 | themeColor = themeList[index]; 50 | themeIndex = index; 51 | GlobalValue.setThemeIndex(index); 52 | } 53 | 54 | ///暗黑模式:系统自身设置 和 用户在App设置 55 | ///只有在系统设置 不是暗黑模式 时,用户才可以在App中设置 56 | bool _darkMode = true; 57 | 58 | bool get darkMode => _darkMode; 59 | 60 | set darkMode(bool value) { 61 | _darkMode = value; 62 | notifyListeners(); 63 | } 64 | 65 | ///主题数据:主题颜色 和 暗黑模式 组合生成 66 | ThemeData get themeData { 67 | Brightness brightness = darkMode ? Brightness.dark : Brightness.light; 68 | 69 | ///暗黑主题 70 | ColorScheme dark = const ColorScheme.dark().copyWith( 71 | primary: themeColor[800], 72 | primaryVariant: themeColor[900], 73 | secondary: themeColor[800], 74 | secondaryVariant: themeColor[900], 75 | ); 76 | 77 | ///高亮主题 78 | ColorScheme light = const ColorScheme.light().copyWith( 79 | primary: themeColor, 80 | primaryVariant: themeColor[700], 81 | secondary: themeColor, 82 | secondaryVariant: themeColor[700], 83 | ); 84 | 85 | ///主题颜色值 86 | Color color = darkMode ? themeColor[900]! : themeColor; 87 | 88 | ThemeData theme = ThemeData( 89 | ///暗黑模式 90 | brightness: brightness, 91 | 92 | ///主题颜色 93 | primarySwatch: themeColor, 94 | primaryColor: themeColor, 95 | colorScheme: darkMode ? dark : light, 96 | 97 | ///单选按钮之类的颜色值 98 | toggleableActiveColor: color, 99 | 100 | ///导航栏指示器颜色 101 | indicatorColor: darkMode ? themeColor : Colors.white, 102 | 103 | ///文本主题 104 | textTheme: TextTheme( 105 | //item_main:18/black 106 | titleMedium: TextStyle( 107 | fontSize: 18, 108 | color: darkMode ? Colors.grey[100] : Colors.grey[900]), 109 | //item_sub:16/grey 110 | bodyMedium: TextStyle( 111 | fontSize: 16, color: darkMode ? Colors.grey : Colors.grey), 112 | //label_main:14/midBlack 113 | labelMedium: TextStyle( 114 | fontSize: 14, 115 | color: darkMode ? Colors.grey[300] : Colors.grey[700]), 116 | ), 117 | 118 | ///字体样式 119 | // fontFamily: 'Georgia', 120 | ); 121 | return theme; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/helper/router_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_wan_android/utils/log_util.dart'; 5 | 6 | ///路由辅助类 7 | ///Navigator.push/pushXX 定义 T 会报错,此处去除泛型定义,直接只用 dynamic 8 | class RouterHelper { 9 | ///参考一下: https://blog.csdn.net/kk_yanwu/article/details/116030668 10 | 11 | ///remove移除路由:待实现 12 | 13 | /// 根据路由名称导航 14 | /// 需要在 MaterialApp 中添加路由表 15 | /// return MaterialApp( 16 | /// routes: { 17 | /// // xx 名称对应 xxPage 页面 18 | /// "xxName": (BuildContext context) => xxPage(), 19 | /// }, 20 | /// ); 21 | 22 | ///直接传递页面进行导航 23 | static Future push(BuildContext context, Widget widget, 24 | {bool fullscreenDialog = false}) { 25 | return Navigator.push( 26 | context, 27 | MaterialPageRoute( 28 | builder: (context) { 29 | return widget; 30 | }, 31 | 32 | /// 全屏弹窗效果,从下往上出现 33 | fullscreenDialog: fullscreenDialog, 34 | )); 35 | } 36 | 37 | ///根据路由名称导航 38 | static Future pushNamed(BuildContext context, String routeName, 39 | {Object? arguments}) { 40 | ///此处定义强类型会报错 41 | return Navigator.pushNamed(context, routeName, arguments: arguments); 42 | } 43 | 44 | ///替换栈顶(当前)页面 45 | static Future pushReplacementNamed(BuildContext context, String routeName, 46 | {Object? arguments}) { 47 | return Navigator.pushReplacementNamed(context, routeName, 48 | arguments: arguments); 49 | } 50 | 51 | ///跳转到页面移除其他所有界面(除了指定的页面) 52 | ///说明:当前栈情况:p1=>p2=>p3=>p4 目标界面 routeName = p9 保留界面 p2 53 | ///执行之后移除 p1=>p3=>p4 只剩下 p1 以及新增加的 p9 54 | static Future pushNamedAndRemoveUntil( 55 | BuildContext context, String routeName, String? untilName, 56 | {Object? arguments}) { 57 | /// predicate = false 全部不移除 58 | /// predicate = true 全部移除 59 | /// predicate = ModalRoute.withName(untilName); 指定保留页面 60 | RoutePredicate predicate; 61 | if (untilName == null || untilName.isEmpty) { 62 | predicate = (Route route) => false; 63 | } else { 64 | predicate = ModalRoute.withName(untilName); 65 | } 66 | 67 | return Navigator.pushNamedAndRemoveUntil(context, routeName, predicate, 68 | arguments: arguments); 69 | } 70 | 71 | ///从当前开始关闭页面,一直返回到目标页面 72 | ///说明:当前栈情况:p1=>p2=>p3=>p4 回到目标界面 untilName = p1 73 | ///执行之后移除 p2=>p3=>p4 只剩下 p1 74 | static void popUntil(BuildContext context, String? untilName) { 75 | RoutePredicate predicate; 76 | 77 | ///untilName = null 直接回到首页 78 | if (untilName == null || untilName.isEmpty) { 79 | predicate = (Route route) { 80 | return route.isFirst; 81 | }; 82 | } else { 83 | predicate = ModalRoute.withName(untilName); 84 | } 85 | Navigator.popUntil(context, predicate); 86 | } 87 | 88 | ///关闭页面 89 | static void pop(BuildContext context, [T? result]) { 90 | Navigator.pop(context, result); 91 | } 92 | 93 | ///获取参数:map 94 | static Map argumentsMap(BuildContext context) { 95 | Map map = {}; 96 | 97 | Object? object = ModalRoute.of(context)?.settings.arguments; 98 | if (object != null) { 99 | try { 100 | map = json.decode(json.encode(object)); 101 | } catch (e) { 102 | Logger.log(e); 103 | } 104 | } 105 | return map; 106 | } 107 | 108 | ///获取参数:T 109 | ///传递参数时明确传递了某个 T 值 110 | static T? argumentsT(BuildContext context) { 111 | Object? object = ModalRoute.of(context)?.settings.arguments; 112 | if (object == null) return null; 113 | return object as T; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/modules/main/view/main_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/helper/image_helper.dart'; 3 | import 'package:flutter_wan_android/modules/book/view/main_book.dart'; 4 | import 'package:flutter_wan_android/modules/home/view/main_home.dart'; 5 | import 'package:flutter_wan_android/modules/knowledge/view/main_knowledge.dart'; 6 | import 'package:flutter_wan_android/modules/me/view/main_me.dart'; 7 | import 'package:flutter_wan_android/modules/project/view/main_project.dart'; 8 | 9 | import '../../../generated/l10n.dart'; 10 | 11 | class MainPage extends StatefulWidget { 12 | const MainPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _MainPageState(); 16 | } 17 | 18 | class _MainPageState extends State { 19 | int _navBarIndex = 0; 20 | final PageController _pageController = PageController(); 21 | 22 | MainHomePage? _homePage; 23 | MainProjectPage? _projectPage; 24 | MainBookPage? _bookPage; 25 | MainKnowledgePage? _knowledgePage; 26 | MainMePage? _mePage; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Scaffold( 36 | body: _bodyContent(), 37 | bottomNavigationBar: _bottomNavigationBar(), 38 | ); 39 | } 40 | 41 | @override 42 | void dispose() { 43 | _pageController.dispose(); 44 | super.dispose(); 45 | } 46 | 47 | Widget _bodyContent() { 48 | return PageView.builder( 49 | controller: _pageController, 50 | physics: const NeverScrollableScrollPhysics(), 51 | itemCount: 5, 52 | itemBuilder: (BuildContext context, int index) { 53 | if (index == 0) { 54 | return _homePage ??= const MainHomePage(); 55 | } 56 | if (index == 1) { 57 | return _projectPage ??= const MainProjectPage(); 58 | } 59 | if (index == 2) { 60 | return _bookPage ??= const MainBookPage(); 61 | } 62 | if (index == 3) { 63 | return _knowledgePage ??= const MainKnowledgePage(); 64 | } 65 | if (index == 4) { 66 | return _mePage ??= const MainMePage(); 67 | } 68 | return Container(); 69 | }); 70 | } 71 | 72 | /// 底部导航栏bar 73 | Widget _bottomNavigationBar() { 74 | return BottomNavigationBar( 75 | items: [ 76 | _bottomNavigationBarItem(S.of(context).tab_home, "ic_tab_home.png"), 77 | _bottomNavigationBarItem( 78 | S.of(context).tab_project, "ic_tab_project.png"), 79 | _bottomNavigationBarItem(S.of(context).tab_book, "ic_tab_book.png"), 80 | _bottomNavigationBarItem( 81 | S.of(context).tab_knowledge, "ic_tab_knowledge.png"), 82 | _bottomNavigationBarItem(S.of(context).tab_me, "ic_tab_me.png"), 83 | ], 84 | selectedItemColor: Theme.of(context).primaryColor, 85 | currentIndex: _navBarIndex, 86 | // backgroundColor: Colors.white, 87 | type: BottomNavigationBarType.fixed, 88 | onTap: (index) { 89 | setState(() { 90 | _navBarIndex = index; 91 | _pageController.jumpToPage(index); 92 | }); 93 | }, 94 | ); 95 | } 96 | 97 | ///home menu_book perm_identity 98 | /// 底部导航栏按钮 99 | BottomNavigationBarItem _bottomNavigationBarItem(String label, String icon) { 100 | return BottomNavigationBarItem( 101 | icon: Image.asset(ImageHelper.wrapAssets(icon), 102 | height: 24, width: 24, color: Colors.grey), 103 | activeIcon: Image.asset(ImageHelper.wrapAssets(icon), 104 | height: 24, width: 24, color: Theme.of(context).primaryColor), 105 | label: label, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/core/net/http_request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:dio_cookie_manager/dio_cookie_manager.dart'; 5 | import 'package:flutter_wan_android/helper/cookie_helper.dart'; 6 | 7 | import '../../utils/log_util.dart'; 8 | import '../net/http_config.dart'; 9 | import '../net/observer/http_lifecycle_observer.dart'; 10 | import 'cancel/zt_http_cancel.dart'; 11 | 12 | class HttpRequest { 13 | /// 全局请求配置 14 | /// URL/超时时间/headers... 15 | static final BaseOptions baseOptions = BaseOptions( 16 | baseUrl: HttpConfig.baseUrl, connectTimeout: HttpConfig.timeout); 17 | 18 | ///dio实例 19 | static final Dio dio = Dio(baseOptions); 20 | 21 | /// http取消管理类 22 | static final HttpCancelManager httpCancelManager = HttpCancelManager(); 23 | 24 | /// GET 25 | static Future get( 26 | String url, { 27 | HttpCanceler? canceler, 28 | data, 29 | Map? params, 30 | Interceptor? inter, 31 | ProgressCallback? sendProgress, 32 | ProgressCallback? receiveProgress, 33 | }) async { 34 | /// 检查是否需要生命周期感知 35 | checkLifecycleAware(canceler); 36 | 37 | return await request(url, 38 | method: "get", 39 | data: data, 40 | params: params, 41 | inter: inter, 42 | token: canceler?.cancelToken, 43 | sendProgress: sendProgress, 44 | receiveProgress: receiveProgress); 45 | } 46 | 47 | /// POST 48 | static Future post( 49 | String url, { 50 | HttpCanceler? canceler, 51 | data, 52 | Map? params, 53 | Interceptor? inter, 54 | ProgressCallback? sendProgress, 55 | ProgressCallback? receiveProgress, 56 | }) async { 57 | /// 检查是否需要生命周期感知 58 | checkLifecycleAware(canceler); 59 | 60 | return request(url, 61 | method: "post", 62 | data: data, 63 | params: params, 64 | inter: inter, 65 | token: canceler?.cancelToken, 66 | sendProgress: sendProgress, 67 | receiveProgress: receiveProgress); 68 | } 69 | 70 | /// 网络请求最终实现 71 | static Future request( 72 | String url, { 73 | String method = "get", 74 | data, 75 | Map? params, 76 | Interceptor? inter, 77 | CancelToken? token, 78 | ProgressCallback? sendProgress, 79 | ProgressCallback? receiveProgress, 80 | }) async { 81 | // 1.请求的单独配置 82 | final options = Options(method: method); 83 | 84 | // 2.添加拦截器:添加日志,处理响应数据等 85 | Interceptor dInter = InterceptorsWrapper( 86 | onRequest: (RequestOptions options, RequestInterceptorHandler handler) { 87 | Logger.log("Request:"); 88 | Logger.log("url = ${options.baseUrl}${options.path}"); 89 | Logger.log("params = ${json.encode(options.queryParameters)}"); 90 | //这里处理请求拦截器,默认直接next 91 | handler.next(options); 92 | }, onResponse: (Response e, ResponseInterceptorHandler handler) { 93 | Logger.log("Response:"); 94 | Logger.log(json.encode(e.data)); 95 | //这里处理结果拦截 96 | handler.next(e); 97 | }, onError: (DioError e, ErrorInterceptorHandler handler) { 98 | //这里处理错误拦截 99 | handler.next(e); 100 | }); 101 | 102 | List inters = [dInter]; 103 | if (inter != null) { 104 | inters.add(inter); 105 | } 106 | 107 | /// 此处不能无限叠加拦截器 108 | dio.interceptors.clear(); 109 | dio.interceptors.addAll(inters); 110 | 111 | ///添加cookie 112 | dio.interceptors.add(CookieManager(await CookieHelper.cookieJar)); 113 | 114 | // 3.发送网络请求 115 | try { 116 | Response response = await dio.request(url, 117 | queryParameters: params, 118 | data: data, 119 | options: options, 120 | cancelToken: token, 121 | onSendProgress: sendProgress, 122 | onReceiveProgress: receiveProgress); 123 | return response.data; 124 | } on DioError catch (e) { 125 | return Future.error(e); 126 | } 127 | } 128 | 129 | /// 检查是否需要生命周期感知 130 | /// 存在标识需要管理生命周期,在 页面销毁时/指定生命周期中 取消网络请求 131 | static void checkLifecycleAware(HttpCanceler? canceler) { 132 | /// 需要管理生命周期 133 | if (canceler != null) { 134 | httpCancelManager.bindCancel(canceler.lifecycleOwner, canceler); 135 | canceler.lifecycleOwner 136 | .getLifecycle() 137 | .addObserver(HttpLifecycleObserver(httpCancelManager, canceler)); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_zh.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a zh locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'zh'; 22 | 23 | static String m0(value) => "积分:${value}"; 24 | 25 | static String m1(main, sub) => "${main} - ${sub}"; 26 | 27 | static String m2(progress) => "已学${progress}%"; 28 | 29 | final messages = _notInlinedMessages(_notInlinedMessages); 30 | static Map _notInlinedMessages(_) => { 31 | "account_empty_tip": MessageLookupByLibrary.simpleMessage("请输入账号"), 32 | "cancel": MessageLookupByLibrary.simpleMessage("取消"), 33 | "clean_all": MessageLookupByLibrary.simpleMessage("清除全部"), 34 | "collect": MessageLookupByLibrary.simpleMessage("收藏"), 35 | "collect_content": MessageLookupByLibrary.simpleMessage("您确定要移除收藏内容吗?"), 36 | "color_theme": MessageLookupByLibrary.simpleMessage("彩色主题"), 37 | "confirm": MessageLookupByLibrary.simpleMessage("确定"), 38 | "dark_style": MessageLookupByLibrary.simpleMessage("暗黑模式"), 39 | "done": MessageLookupByLibrary.simpleMessage("完成"), 40 | "edit": MessageLookupByLibrary.simpleMessage("编辑"), 41 | "integral": m0, 42 | "label_group": m1, 43 | "language_chinese": MessageLookupByLibrary.simpleMessage("中文"), 44 | "language_english": MessageLookupByLibrary.simpleMessage("英文"), 45 | "learn_no": MessageLookupByLibrary.simpleMessage("未学习"), 46 | "learn_progress": m2, 47 | "loading_content": MessageLookupByLibrary.simpleMessage("内容加载中..."), 48 | "login": MessageLookupByLibrary.simpleMessage("登录"), 49 | "login_success": MessageLookupByLibrary.simpleMessage("登录成功"), 50 | "multi_language": MessageLookupByLibrary.simpleMessage("多语言"), 51 | "net_error": MessageLookupByLibrary.simpleMessage("网络错误"), 52 | "no_account": MessageLookupByLibrary.simpleMessage("还没账号?"), 53 | "placeholder": MessageLookupByLibrary.simpleMessage("--"), 54 | "psw_confirm_empty_tip": MessageLookupByLibrary.simpleMessage("请确认密码"), 55 | "psw_confirm_tip": MessageLookupByLibrary.simpleMessage("两次密码不一致"), 56 | "psw_empty_tip": MessageLookupByLibrary.simpleMessage("请输入密码"), 57 | "register": MessageLookupByLibrary.simpleMessage("注册"), 58 | "register_now": MessageLookupByLibrary.simpleMessage("立即注册"), 59 | "register_success": MessageLookupByLibrary.simpleMessage("注册成功"), 60 | "search_hint": MessageLookupByLibrary.simpleMessage("用空格分隔多个关键词"), 61 | "search_hot_title": MessageLookupByLibrary.simpleMessage("热门搜索"), 62 | "search_local_title": MessageLookupByLibrary.simpleMessage("历史搜索"), 63 | "settings": MessageLookupByLibrary.simpleMessage("设置"), 64 | "tab_book": MessageLookupByLibrary.simpleMessage("教程"), 65 | "tab_book_course": MessageLookupByLibrary.simpleMessage("书籍教程"), 66 | "tab_home": MessageLookupByLibrary.simpleMessage("首页"), 67 | "tab_knowledge": MessageLookupByLibrary.simpleMessage("知识"), 68 | "tab_me": MessageLookupByLibrary.simpleMessage("我的"), 69 | "tab_nav": MessageLookupByLibrary.simpleMessage("导航"), 70 | "tab_project": MessageLookupByLibrary.simpleMessage("项目"), 71 | "tab_tree": MessageLookupByLibrary.simpleMessage("体系"), 72 | "tips_msg": MessageLookupByLibrary.simpleMessage("提示"), 73 | "topping": MessageLookupByLibrary.simpleMessage("置顶"), 74 | "user_name": MessageLookupByLibrary.simpleMessage("用户名"), 75 | "user_psw": MessageLookupByLibrary.simpleMessage("密码"), 76 | "user_psw_confirm": MessageLookupByLibrary.simpleMessage("确认密码") 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_en.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that provides messages for a en locale. All the 3 | // messages from the main program should be duplicated here with the same 4 | // function name. 5 | 6 | // Ignore issues from commonly used lints in this file. 7 | // ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new 8 | // ignore_for_file:prefer_single_quotes,comment_references, directives_ordering 9 | // ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases 10 | // ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes 11 | // ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes 12 | 13 | import 'package:intl/intl.dart'; 14 | import 'package:intl/message_lookup_by_library.dart'; 15 | 16 | final messages = new MessageLookup(); 17 | 18 | typedef String MessageIfAbsent(String messageStr, List args); 19 | 20 | class MessageLookup extends MessageLookupByLibrary { 21 | String get localeName => 'en'; 22 | 23 | static String m0(value) => "Integral:${value}"; 24 | 25 | static String m1(main, sub) => "${main} - ${sub}"; 26 | 27 | static String m2(progress) => "Learned${progress}%"; 28 | 29 | final messages = _notInlinedMessages(_notInlinedMessages); 30 | static Map _notInlinedMessages(_) => { 31 | "account_empty_tip": 32 | MessageLookupByLibrary.simpleMessage("Please enter an account"), 33 | "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), 34 | "clean_all": MessageLookupByLibrary.simpleMessage("Clear all"), 35 | "collect": MessageLookupByLibrary.simpleMessage("Collect"), 36 | "collect_content": MessageLookupByLibrary.simpleMessage( 37 | "Are you sure remove the collected content?"), 38 | "color_theme": MessageLookupByLibrary.simpleMessage("Color Theme"), 39 | "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), 40 | "dark_style": MessageLookupByLibrary.simpleMessage("Dark Mode"), 41 | "done": MessageLookupByLibrary.simpleMessage("Done"), 42 | "edit": MessageLookupByLibrary.simpleMessage("Edit"), 43 | "integral": m0, 44 | "label_group": m1, 45 | "language_chinese": MessageLookupByLibrary.simpleMessage("Chinese"), 46 | "language_english": MessageLookupByLibrary.simpleMessage("English"), 47 | "learn_no": MessageLookupByLibrary.simpleMessage("No learned"), 48 | "learn_progress": m2, 49 | "loading_content": 50 | MessageLookupByLibrary.simpleMessage("content loading..."), 51 | "login": MessageLookupByLibrary.simpleMessage("Login"), 52 | "login_success": MessageLookupByLibrary.simpleMessage("Login success"), 53 | "multi_language": 54 | MessageLookupByLibrary.simpleMessage("Multi language"), 55 | "net_error": MessageLookupByLibrary.simpleMessage("Network error"), 56 | "no_account": MessageLookupByLibrary.simpleMessage("No account yet?"), 57 | "placeholder": MessageLookupByLibrary.simpleMessage("--"), 58 | "psw_confirm_empty_tip": 59 | MessageLookupByLibrary.simpleMessage("Please confirm the password"), 60 | "psw_confirm_tip": MessageLookupByLibrary.simpleMessage( 61 | "Two passwords are inconsistent"), 62 | "psw_empty_tip": 63 | MessageLookupByLibrary.simpleMessage("Please enter an password"), 64 | "register": MessageLookupByLibrary.simpleMessage("Register"), 65 | "register_now": MessageLookupByLibrary.simpleMessage("Register now"), 66 | "register_success": 67 | MessageLookupByLibrary.simpleMessage("Register success"), 68 | "search_hint": MessageLookupByLibrary.simpleMessage( 69 | "separate multiple keywords with spaces"), 70 | "search_hot_title": 71 | MessageLookupByLibrary.simpleMessage("Popular search"), 72 | "search_local_title": 73 | MessageLookupByLibrary.simpleMessage("Historical search"), 74 | "settings": MessageLookupByLibrary.simpleMessage("Settings"), 75 | "tab_book": MessageLookupByLibrary.simpleMessage("Book"), 76 | "tab_book_course": 77 | MessageLookupByLibrary.simpleMessage("Book tutorial"), 78 | "tab_home": MessageLookupByLibrary.simpleMessage("Home"), 79 | "tab_knowledge": MessageLookupByLibrary.simpleMessage("Knowledge"), 80 | "tab_me": MessageLookupByLibrary.simpleMessage("Me"), 81 | "tab_nav": MessageLookupByLibrary.simpleMessage("Navigation"), 82 | "tab_project": MessageLookupByLibrary.simpleMessage("Project"), 83 | "tab_tree": MessageLookupByLibrary.simpleMessage("System"), 84 | "tips_msg": MessageLookupByLibrary.simpleMessage("Tips"), 85 | "topping": MessageLookupByLibrary.simpleMessage("Topping"), 86 | "user_name": MessageLookupByLibrary.simpleMessage("UserName"), 87 | "user_psw": MessageLookupByLibrary.simpleMessage("Password"), 88 | "user_psw_confirm": 89 | MessageLookupByLibrary.simpleMessage("Confirm Password") 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /lib/generated/json/base/json_convert_content.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names 2 | // ignore_for_file: camel_case_types 3 | // ignore_for_file: prefer_single_quotes 4 | 5 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 6 | import 'package:flutter/material.dart' show debugPrint; 7 | import 'package:flutter_wan_android/modules/account/model/user_entity.dart'; 8 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 9 | import 'package:flutter_wan_android/modules/book/model/book_entity.dart'; 10 | import 'package:flutter_wan_android/modules/home/model/banner_entity.dart'; 11 | import 'package:flutter_wan_android/modules/knowledge/model/nav_entity.dart'; 12 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 13 | import 'package:flutter_wan_android/modules/search/model/search_entity.dart'; 14 | 15 | JsonConvert jsonConvert = JsonConvert(); 16 | typedef JsonConvertFunction = T Function(Map json); 17 | 18 | class JsonConvert { 19 | static final Map _convertFuncMap = { 20 | (UserEntity).toString(): UserEntity.fromJson, 21 | (ArticleEntity).toString(): ArticleEntity.fromJson, 22 | (BookEntity).toString(): BookEntity.fromJson, 23 | (BannerEntity).toString(): BannerEntity.fromJson, 24 | (NavEntity).toString(): NavEntity.fromJson, 25 | (CategoryEntity).toString(): CategoryEntity.fromJson, 26 | (SearchEntity).toString(): SearchEntity.fromJson, 27 | }; 28 | 29 | T? convert(dynamic value) { 30 | if (value == null) { 31 | return null; 32 | } 33 | return asT(value); 34 | } 35 | 36 | List? convertList(List? value) { 37 | if (value == null) { 38 | return null; 39 | } 40 | try { 41 | return value.map((dynamic e) => asT(e)).toList(); 42 | } catch (e, stackTrace) { 43 | debugPrint('asT<$T> $e $stackTrace'); 44 | return []; 45 | } 46 | } 47 | 48 | List? convertListNotNull(dynamic value) { 49 | if (value == null) { 50 | return null; 51 | } 52 | try { 53 | return (value as List).map((dynamic e) => asT(e)!).toList(); 54 | } catch (e, stackTrace) { 55 | debugPrint('asT<$T> $e $stackTrace'); 56 | return []; 57 | } 58 | } 59 | 60 | T? asT(dynamic value) { 61 | if (value is T) { 62 | return value; 63 | } 64 | final String type = T.toString(); 65 | try { 66 | final String valueS = value.toString(); 67 | if (type == "String") { 68 | return valueS as T; 69 | } else if (type == "int") { 70 | final int? intValue = int.tryParse(valueS); 71 | if (intValue == null) { 72 | return double.tryParse(valueS)?.toInt() as T?; 73 | } else { 74 | return intValue as T; 75 | } 76 | } else if (type == "double") { 77 | return double.parse(valueS) as T; 78 | } else if (type == "DateTime") { 79 | return DateTime.parse(valueS) as T; 80 | } else if (type == "bool") { 81 | if (valueS == '0' || valueS == '1') { 82 | return (valueS == '1') as T; 83 | } 84 | return (valueS == 'true') as T; 85 | } else if (type == "Map" || type.startsWith("Map<")) { 86 | return value as T; 87 | } else { 88 | if (_convertFuncMap.containsKey(type)) { 89 | return _convertFuncMap[type]!(value) as T; 90 | } else { 91 | throw UnimplementedError('$type unimplemented'); 92 | } 93 | } 94 | } catch (e, stackTrace) { 95 | debugPrint('asT<$T> $e $stackTrace'); 96 | return null; 97 | } 98 | } 99 | 100 | //list is returned by type 101 | static M? _getListChildType(List> data) { 102 | if([] is M){ 103 | return data.map((Map e) => UserEntity.fromJson(e)).toList() as M; 104 | } 105 | if([] is M){ 106 | return data.map((Map e) => ArticleEntity.fromJson(e)).toList() as M; 107 | } 108 | if([] is M){ 109 | return data.map((Map e) => BookEntity.fromJson(e)).toList() as M; 110 | } 111 | if([] is M){ 112 | return data.map((Map e) => BannerEntity.fromJson(e)).toList() as M; 113 | } 114 | if([] is M){ 115 | return data.map((Map e) => NavEntity.fromJson(e)).toList() as M; 116 | } 117 | if([] is M){ 118 | return data.map((Map e) => CategoryEntity.fromJson(e)).toList() as M; 119 | } 120 | if([] is M){ 121 | return data.map((Map e) => SearchEntity.fromJson(e)).toList() as M; 122 | } 123 | 124 | debugPrint("${M.toString()} not found"); 125 | 126 | return null; 127 | } 128 | 129 | static M? fromJsonAsT(dynamic json) { 130 | if (json is List) { 131 | return _getListChildType(json.map((e) => e as Map).toList()); 132 | } else { 133 | return jsonConvert.asT(json); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /lib/modules/home/widget/banner_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle.dart'; 5 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 6 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 7 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 8 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 9 | import 'package:flutter_wan_android/modules/home/model/banner_entity.dart'; 10 | import 'package:flutter_wan_android/modules/home/model/home_model.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | import '../../../helper/image_helper.dart'; 14 | import '../../../utils/log_util.dart'; 15 | 16 | ///Banner控件 17 | class BannerWidget extends StatefulWidget { 18 | const BannerWidget({Key? key}) : super(key: key); 19 | 20 | @override 21 | State createState() => _BannerWidgetState(); 22 | } 23 | 24 | class _BannerWidgetState extends State with Lifecycle { 25 | final BannerViewModel bannerViewModel = BannerViewModel(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | getLifecycle().addObserver(bannerViewModel); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return MultiProvider( 36 | providers: [ChangeNotifierProvider(create: (context) => bannerViewModel)], 37 | child: bannerWidget(), 38 | ); 39 | } 40 | 41 | Widget bannerWidget() { 42 | return Stack( 43 | children: [ 44 | ///banner 45 | Positioned(child: pageView()), 46 | 47 | ///指示器 48 | Align(alignment: Alignment.bottomCenter, child: indicator()) 49 | ], 50 | ); 51 | } 52 | 53 | ///指示器 54 | Widget indicator() { 55 | return Consumer(builder: (context, viewModel, child) { 56 | return Row( 57 | mainAxisAlignment: MainAxisAlignment.center, 58 | children: indicatorChild(viewModel)); 59 | }); 60 | } 61 | 62 | ///指示器子控件 63 | List indicatorChild(BannerViewModel viewModel) { 64 | List children = []; 65 | 66 | for (int i = 0; i < viewModel.dataArray.length; i++) { 67 | children.add(Container( 68 | padding: const EdgeInsets.all(1), 69 | child: Icon( 70 | Icons.circle, 71 | size: 10, 72 | color: viewModel.index == i 73 | ? Theme.of(context).primaryColor 74 | : Colors.grey, 75 | ), 76 | )); 77 | } 78 | return children; 79 | } 80 | 81 | ///pageView 82 | Widget pageView() { 83 | return Consumer(builder: (context, viewModel, child) { 84 | return PageView.builder( 85 | controller: viewModel.controller, 86 | itemBuilder: (BuildContext context, int index) { 87 | if (viewModel.dataArray.isNotEmpty) { 88 | return ImageHelper.network( 89 | viewModel 90 | .dataArray[index % viewModel.dataArray.length].imagePath, 91 | fit: BoxFit.cover); 92 | } else { 93 | return Container(); 94 | } 95 | }, 96 | onPageChanged: (int index) { 97 | viewModel.onPageChanged(index); 98 | }, 99 | itemCount: viewModel.itemCount, 100 | ); 101 | }); 102 | } 103 | } 104 | 105 | ///BannerViewModel 106 | class BannerViewModel extends ChangeNotifier with LifecycleObserver { 107 | ///循环数量 108 | final int _loopCount = 10; 109 | 110 | ///循环下标 111 | int _loopIndex = 0; 112 | 113 | ///切换时间 114 | final Duration _duration = const Duration(milliseconds: 1500); 115 | 116 | ///banner数据列表 117 | List _dataArray = []; 118 | 119 | List get dataArray => _dataArray; 120 | 121 | set dataArray(List value) { 122 | _dataArray = value; 123 | _loopIndex = _resumeMidIndex(); 124 | startLoop(); 125 | notifyListeners(); 126 | } 127 | 128 | HomeModel model = HomeModel(); 129 | late HttpCanceler httpCanceler; 130 | 131 | ///获取banner列表 132 | void getBannerList() { 133 | model.getBannerList(httpCanceler).then((value) { 134 | if (value.success) { 135 | dataArray = value.list!; 136 | } 137 | }); 138 | } 139 | 140 | ///控制器 141 | PageController controller = PageController(); 142 | 143 | ///Timer 144 | Timer? _timer; 145 | 146 | ///开始定时器 147 | void startLoop() { 148 | _timer ??= Timer.periodic(_duration, (timer) { 149 | try { 150 | ///下一页 151 | controller.nextPage(duration: _duration, curve: Curves.linear); 152 | } catch (e) { 153 | Logger.log(e); 154 | } 155 | }); 156 | } 157 | 158 | ///取消定时器 159 | void stopLoop() { 160 | _timer?.cancel(); 161 | } 162 | 163 | ///设置loop下标 164 | void _setLoopIndex(int index) { 165 | ///到达第一个数据 166 | if (index == 0) index = _resumeMidIndex(); 167 | 168 | ///到达最后一个数据 169 | if (index == _loopCount - 1) index = _resumeMidIndex(end: true); 170 | 171 | _loopIndex = index; 172 | controller.jumpToPage(_loopIndex); 173 | notifyListeners(); 174 | } 175 | 176 | ///恢复到中间下标 177 | ///依据条件数据 开始/结束 处 178 | ///默认恢复到数据开始下标 179 | int _resumeMidIndex({bool end = false}) { 180 | ///除后取整 181 | int minIndex = _loopCount ~/ 2; 182 | 183 | ///余数 184 | int remainder = minIndex % _dataArray.length; 185 | 186 | ///1.恢复到开始:减去余数 187 | if (!end) { 188 | minIndex = minIndex - remainder; 189 | } 190 | 191 | ///2.恢复到结束:减去余数 - 1(再往前一个) 192 | if (end) { 193 | minIndex = minIndex - remainder - 1; 194 | } 195 | return minIndex; 196 | } 197 | 198 | ///销毁/释放资源 199 | void destroy() { 200 | controller.dispose(); 201 | stopLoop(); 202 | } 203 | 204 | ///PageView页面更新 205 | void onPageChanged(int index) { 206 | _setLoopIndex(index); 207 | } 208 | 209 | ///实际下标值 210 | int get index => _dataArray.isEmpty ? 0 : _loopIndex % _dataArray.length; 211 | 212 | ///item数量 213 | int get itemCount => _loopCount; 214 | 215 | @override 216 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 217 | if (state == LifecycleState.onInit) { 218 | httpCanceler = HttpCanceler(owner); 219 | } else if (state == LifecycleState.onCreate) { 220 | getBannerList(); 221 | } else if (state == LifecycleState.onDestroy) { 222 | destroy(); 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /lib/modules/article/widget/item_article_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_wan_android/helper/router_helper.dart'; 3 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 4 | import 'package:flutter_wan_android/utils/string_util.dart'; 5 | 6 | import '../../../config/router_config.dart'; 7 | import '../../../generated/l10n.dart'; 8 | import '../../../helper/image_helper.dart'; 9 | 10 | ///文章Item控件 11 | class ItemArticleWidget extends StatefulWidget { 12 | final ArticleEntity article; 13 | 14 | ///收藏事件回调 15 | GestureTapCallback? onTapCollect; 16 | 17 | ItemArticleWidget({Key? key, required this.article, this.onTapCollect}) 18 | : super(key: key); 19 | 20 | @override 21 | State createState() => _ItemArticleWidgetState(); 22 | } 23 | 24 | class _ItemArticleWidgetState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | Color? bgColor; 28 | if (widget.article.isTop != null && widget.article.isTop == true) { 29 | bgColor = Theme.of(context).brightness == Brightness.dark 30 | ? Colors.black.withOpacity(0.2) 31 | : Theme.of(context).primaryColor.withOpacity(0.1); 32 | } 33 | return Container( 34 | color: bgColor, 35 | child: GestureDetector( 36 | onTap: () => actionItemClick(), 37 | child: itemWidget(context, widget.article), 38 | )); 39 | } 40 | 41 | void actionItemClick() { 42 | RouterHelper.pushNamed(context, RouterConfig.articleDetailsPage, 43 | arguments: widget.article); 44 | } 45 | 46 | Widget itemWidget(BuildContext context, ArticleEntity article) { 47 | return Padding( 48 | padding: const EdgeInsets.symmetric(horizontal: 20), 49 | child: Column( 50 | children: [ 51 | const SizedBox(height: 10), 52 | 53 | ///顶部信息 54 | itemHeaderWidget(context, article), 55 | 56 | const SizedBox(height: 6), 57 | 58 | ///内容 59 | itemContentWidget(context, article), 60 | 61 | const SizedBox(height: 4), 62 | 63 | ///底部信息 64 | itemFooterWidget(context, article), 65 | 66 | ///分割线 67 | itemSeparatorWidget(), 68 | ], 69 | )); 70 | } 71 | 72 | Widget itemHeaderWidget(BuildContext context, ArticleEntity article) { 73 | return Row( 74 | crossAxisAlignment: CrossAxisAlignment.center, 75 | children: [ 76 | //作者头像 77 | ClipOval( 78 | child: ImageHelper.network(article.userIcon ?? "", 79 | height: 40, width: 40), 80 | ), 81 | const SizedBox(width: 4), 82 | //作者昵称 83 | Expanded( 84 | child: Text( 85 | article.userName ?? "", 86 | maxLines: 1, 87 | style: Theme.of(context).textTheme.labelMedium, 88 | )), 89 | const SizedBox(width: 4), 90 | //时间 91 | Text( 92 | article.date ?? "", 93 | style: Theme.of(context).textTheme.labelMedium, 94 | ), 95 | ], 96 | ); 97 | } 98 | 99 | Widget itemContentWidget(BuildContext context, ArticleEntity article) { 100 | return Row( 101 | crossAxisAlignment: CrossAxisAlignment.start, 102 | children: [ 103 | Expanded( 104 | child: Column( 105 | crossAxisAlignment: CrossAxisAlignment.start, 106 | children: [ 107 | ///标题 108 | //Html(data: article.title ?? ""), 109 | Text( 110 | StringUtil.removeHtmlLabel(article.title ?? ""), 111 | style: Theme.of(context).textTheme.titleMedium, 112 | maxLines: 2, 113 | overflow: TextOverflow.ellipsis, 114 | ), 115 | 116 | const SizedBox(height: 6), 117 | 118 | ///副标题 119 | Offstage( 120 | offstage: article.desc?.isEmpty ?? true, 121 | child: Text( 122 | StringUtil.removeHtmlLabel(article.desc ?? ""), 123 | style: Theme.of(context).textTheme.bodyMedium, 124 | maxLines: 2, 125 | overflow: TextOverflow.ellipsis, 126 | )), 127 | ], 128 | )), 129 | 130 | const SizedBox(width: 6), 131 | 132 | ///条件性展示控件 133 | Offstage( 134 | offstage: article.cover?.isEmpty ?? true, 135 | child: ImageHelper.network(article.cover ?? "", 136 | height: 100, width: 70, fit: BoxFit.cover)), 137 | ], 138 | ); 139 | } 140 | 141 | ///底部内容控件 142 | Widget itemFooterWidget(BuildContext context, ArticleEntity article) { 143 | IconData icon = Icons.favorite_border; 144 | if (article.collect != null) { 145 | icon = article.collect! ? Icons.favorite : Icons.favorite_border; 146 | } 147 | return Row( 148 | children: [ 149 | ///置顶标识 150 | Offstage( 151 | offstage: !(article.isTop ?? false), 152 | child: Container( 153 | padding: const EdgeInsets.symmetric(horizontal: 2), 154 | decoration: BoxDecoration( 155 | border: Border.all(color: Theme.of(context).primaryColor)), 156 | child: Text( 157 | S.of(context).topping, 158 | style: TextStyle( 159 | fontSize: 12, color: Theme.of(context).primaryColor), 160 | ), 161 | )), 162 | 163 | Offstage( 164 | offstage: !(article.isTop ?? false), 165 | child: const SizedBox(width: 6), 166 | ), 167 | 168 | ///标签 169 | Expanded( 170 | child: Text( 171 | S.of(context).label_group( 172 | article.superChapterName ?? "", article.chapterName ?? ""), 173 | style: Theme.of(context).textTheme.labelMedium, 174 | )), 175 | 176 | ///收藏 177 | GestureDetector( 178 | onTap: widget.onTapCollect, 179 | child: Icon( 180 | icon, 181 | color: Theme.of(context).primaryColor, 182 | ), 183 | ), 184 | ], 185 | ); 186 | } 187 | 188 | ///item分割线 189 | Widget itemSeparatorWidget() { 190 | return Container( 191 | height: 1, 192 | margin: const EdgeInsets.only(top: 10), 193 | color: Theme.of(context).dividerColor, 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/modules/knowledge/view/main_knowledge.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_lifecycle_aware/lifecycle.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 5 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 6 | import 'package:flutter_wan_android/config/router_config.dart'; 7 | import 'package:flutter_wan_android/core/net/cancel/http_canceler.dart'; 8 | import 'package:flutter_wan_android/generated/l10n.dart'; 9 | import 'package:flutter_wan_android/helper/router_helper.dart'; 10 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 11 | import 'package:flutter_wan_android/modules/knowledge/model/knowledge_entity.dart'; 12 | import 'package:flutter_wan_android/modules/knowledge/view_model/knowledge_view_model.dart'; 13 | import 'package:flutter_wan_android/modules/project/model/category_entity.dart'; 14 | import 'package:provider/provider.dart'; 15 | 16 | class MainKnowledgePage extends StatefulWidget { 17 | const MainKnowledgePage({Key? key}) : super(key: key); 18 | 19 | @override 20 | State createState() => _MainKnowledgePageState(); 21 | } 22 | 23 | class _MainKnowledgePageState extends State 24 | with AutomaticKeepAliveClientMixin { 25 | @override 26 | Widget build(BuildContext context) { 27 | super.build(context); 28 | List titleArray = [S.of(context).tab_tree, S.of(context).tab_nav]; 29 | return DefaultTabController( 30 | length: titleArray.length, 31 | child: Scaffold( 32 | body: bodyContent(titleArray), 33 | appBar: AppBar( 34 | title: TabBar( 35 | isScrollable: true, 36 | tabs: List.generate(titleArray.length, (index) { 37 | return Tab(child: Text(titleArray[index])); 38 | }), 39 | ), 40 | ), 41 | )); 42 | } 43 | 44 | Widget bodyContent(List titleArray) { 45 | return TabBarView( 46 | children: List.generate(titleArray.length, (index) { 47 | return KnowledgeItemPage(index == 0 ? system : nav); 48 | })); 49 | } 50 | 51 | @override 52 | bool get wantKeepAlive => true; 53 | } 54 | 55 | ///类别:1:系统,2:导航F 56 | const system = 1, nav = 2; 57 | 58 | class KnowledgeItemPage extends StatefulWidget { 59 | final int type; 60 | 61 | const KnowledgeItemPage(this.type, {Key? key}) : super(key: key); 62 | 63 | @override 64 | State createState() => _KnowledgeItemPageState(); 65 | } 66 | 67 | class _KnowledgeItemPageState extends State 68 | with AutomaticKeepAliveClientMixin, Lifecycle, LifecycleObserver { 69 | late BuildContext _buildContext; 70 | late HttpCanceler httpCanceler; 71 | 72 | @override 73 | void initState() { 74 | super.initState(); 75 | getLifecycle().addObserver(this); 76 | } 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | super.build(context); 81 | 82 | return MultiProvider( 83 | providers: [ 84 | ChangeNotifierProvider(create: (context) => KnowledgeViewModel()) 85 | ], 86 | child: Consumer( 87 | builder: (context, viewModel, child) { 88 | _buildContext = context; 89 | return ListView.builder( 90 | itemBuilder: (context, index) { 91 | return itemWidget(viewModel.entity, index); 92 | }, 93 | itemCount: widget.type == system 94 | ? viewModel.entity.categoryList.length 95 | : viewModel.entity.navList.length, 96 | ); 97 | }, 98 | ), 99 | ); 100 | } 101 | 102 | Widget itemWidget(KnowledgeEntity entity, int itemIndex) { 103 | ///标题 104 | String title = widget.type == system 105 | ? entity.categoryList[itemIndex].name 106 | : entity.navList[itemIndex].name; 107 | 108 | ///标签个数 109 | int length = widget.type == system 110 | ? entity.categoryList[itemIndex].childList!.length 111 | : entity.navList[itemIndex].articles.length; 112 | 113 | return Container( 114 | padding: const EdgeInsets.only(top: 15, left: 15, right: 15), 115 | child: Column( 116 | crossAxisAlignment: CrossAxisAlignment.start, 117 | children: [ 118 | ///标题 119 | Text(title, style: Theme.of(context).textTheme.titleMedium), 120 | 121 | ///标签内容 122 | Wrap( 123 | spacing: 6, 124 | runSpacing: -5, 125 | children: List.generate(length, (index) { 126 | return ActionChip( 127 | label: Text( 128 | widget.type == system 129 | ? entity.categoryList[itemIndex].childList![index].name 130 | : entity.navList[itemIndex].articles[index].title ?? "", 131 | style: Theme.of(context) 132 | .textTheme 133 | .labelMedium 134 | ?.copyWith(fontSize: 15), 135 | ), 136 | onPressed: () { 137 | if (widget.type == system) { 138 | actionCategoryChild( 139 | entity.categoryList[itemIndex], index); 140 | } else { 141 | actionArticleDetails( 142 | entity.navList[itemIndex].articles[index]); 143 | } 144 | }); 145 | }), 146 | ) 147 | ], 148 | ), 149 | ); 150 | } 151 | 152 | void actionCategoryChild(CategoryEntity category, int index) { 153 | Map arguments = { 154 | "entity": category.toString(), 155 | "index": index 156 | }; 157 | RouterHelper.pushNamed(context, RouterConfig.knowledgeChildPage, 158 | arguments: arguments); 159 | } 160 | 161 | void actionArticleDetails(ArticleEntity article) { 162 | RouterHelper.pushNamed(context, RouterConfig.articleDetailsPage, 163 | arguments: article); 164 | } 165 | 166 | @override 167 | bool get wantKeepAlive => true; 168 | 169 | @override 170 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 171 | if (state == LifecycleState.onInit) { 172 | httpCanceler = HttpCanceler(owner); 173 | } else if (state == LifecycleState.onCreate) { 174 | KnowledgeViewModel viewModel = _buildContext.read(); 175 | if (widget.type == nav) { 176 | viewModel.getNavList(httpCanceler); 177 | } else { 178 | viewModel.getCategoryList(httpCanceler); 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/modules/article/view/article_details_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 3 | import 'package:flutter_lifecycle_aware/lifecycle.dart'; 4 | import 'package:flutter_lifecycle_aware/lifecycle_observer.dart'; 5 | import 'package:flutter_lifecycle_aware/lifecycle_owner.dart'; 6 | import 'package:flutter_lifecycle_aware/lifecycle_state.dart'; 7 | import 'package:flutter_wan_android/helper/router_helper.dart'; 8 | import 'package:flutter_wan_android/modules/article/model/article_entity.dart'; 9 | import 'package:flutter_wan_android/modules/article/view_model/article_details_view_model.dart'; 10 | import 'package:flutter_wan_android/utils/string_util.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | import '../../../generated/l10n.dart'; 14 | 15 | ///文章详情页面 16 | class ArticleDetailsPage extends StatefulWidget { 17 | const ArticleDetailsPage({Key? key}) : super(key: key); 18 | 19 | @override 20 | State createState() => _ArticleDetailsPageState(); 21 | } 22 | 23 | class _ArticleDetailsPageState extends State 24 | with Lifecycle, LifecycleObserver { 25 | ///滚动进度 26 | double scrollProgress = 0.0; 27 | 28 | ///页面可滚动内容高度 29 | double scrollHeight = 0.0; 30 | final GlobalKey _globalKeyWebView = GlobalKey(); 31 | late BuildContext _buildContext; 32 | 33 | late InAppWebViewController _webViewController; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | getLifecycle().addObserver(this); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return MultiProvider( 44 | providers: [ 45 | ChangeNotifierProvider(create: (context) => ArticleDetailsViewModel()) 46 | ], 47 | child: Consumer( 48 | builder: (context, viewModel, child) { 49 | _buildContext = context; 50 | return Scaffold( 51 | appBar: appBar(context, viewModel), 52 | body: bodyContent(context, viewModel)); 53 | })); 54 | } 55 | 56 | ///通过浏览器打开 57 | void actionBrowse(BuildContext context, ArticleDetailsViewModel viewModel) {} 58 | 59 | ///收藏 60 | void actionCollect(BuildContext context, ArticleDetailsViewModel viewModel) {} 61 | 62 | ///返回 63 | void actionBack(BuildContext context) { 64 | RouterHelper.pop(context, scrollProgress); 65 | } 66 | 67 | ///导航栏 68 | AppBar appBar(BuildContext context, ArticleDetailsViewModel viewModel) { 69 | return AppBar( 70 | titleSpacing: 0.0, 71 | 72 | ///返回按钮 73 | leading: GestureDetector( 74 | onTap: () => actionBack(context), 75 | child: const Icon(Icons.arrow_back)), 76 | 77 | ///标题 78 | title: Text( 79 | StringUtil.removeHtmlLabel(viewModel.articleEntity?.title ?? ""), 80 | style: const TextStyle(fontSize: 16), 81 | maxLines: 1, 82 | overflow: TextOverflow.ellipsis), 83 | 84 | ///菜单按钮 85 | actions: [ 86 | ///打开浏览器 87 | GestureDetector( 88 | onTap: () => actionBrowse(context, viewModel), 89 | child: Container( 90 | margin: const EdgeInsets.symmetric( 91 | horizontal: NavigationToolbar.kMiddleSpacing), 92 | child: const Icon(Icons.language))), 93 | 94 | ///收藏 95 | GestureDetector( 96 | onTap: () => actionCollect(context, viewModel), 97 | child: Container( 98 | margin: const EdgeInsets.symmetric( 99 | horizontal: NavigationToolbar.kMiddleSpacing), 100 | child: const Icon(Icons.favorite), 101 | )), 102 | ], 103 | ); 104 | } 105 | 106 | ///基本配置 107 | InAppWebViewGroupOptions options = InAppWebViewGroupOptions( 108 | crossPlatform: InAppWebViewOptions(useShouldOverrideUrlLoading: true), 109 | android: AndroidInAppWebViewOptions(useHybridComposition: true), 110 | ios: IOSInAppWebViewOptions(allowsInlineMediaPlayback: true), 111 | ); 112 | 113 | ///内容WebView 114 | Widget bodyContent(BuildContext context, ArticleDetailsViewModel viewModel) { 115 | return Stack(children: [ 116 | ///webView 117 | webContent(context, viewModel), 118 | 119 | ///加载进度 120 | Offstage( 121 | offstage: viewModel.loadProgress == 1.0, 122 | child: placeholderContent(context, viewModel)) 123 | ]); 124 | } 125 | 126 | ///WebView 127 | Widget webContent(BuildContext context, ArticleDetailsViewModel viewModel) { 128 | return InAppWebView( 129 | key: _globalKeyWebView, 130 | initialOptions: options, 131 | /*initialUrlRequest: 132 | URLRequest(url: Uri.parse(viewModel.articleEntity?.link ?? "")),*/ 133 | 134 | ///加载进度 135 | onProgressChanged: (InAppWebViewController controller, int progress) { 136 | viewModel.loadProgress = progress / 100.0; 137 | }, 138 | onWebViewCreated: (InAppWebViewController controller) { 139 | _webViewController = controller; 140 | String url = viewModel.articleEntity?.link ?? ""; 141 | if (url.isNotEmpty) { 142 | _webViewController.loadUrl( 143 | urlRequest: URLRequest(url: Uri.parse(url))); 144 | } 145 | }, 146 | 147 | onLoadStop: (InAppWebViewController controller, Uri? url) async { 148 | int contentHeight = await controller.getContentHeight() ?? 0; 149 | double widgetHeight = 150 | _globalKeyWebView.currentContext?.size?.height ?? 0; 151 | scrollHeight = contentHeight - widgetHeight; 152 | }, 153 | 154 | ///滚动监听 155 | onScrollChanged: (InAppWebViewController controller, int x, int y) { 156 | if (scrollHeight > 0) { 157 | scrollProgress = y / scrollHeight; 158 | } 159 | }, 160 | ); 161 | } 162 | 163 | ///加载中 164 | Widget placeholderContent( 165 | BuildContext context, ArticleDetailsViewModel viewModel) { 166 | return Column( 167 | mainAxisAlignment: MainAxisAlignment.center, 168 | children: [ 169 | CircularProgressIndicator( 170 | value: viewModel.loadProgress, 171 | color: Theme.of(context).primaryColor, 172 | backgroundColor: Colors.grey[300], 173 | ), 174 | const SizedBox(height: 20, width: double.infinity), 175 | Text( 176 | S.of(context).loading_content, 177 | style: TextStyle(fontSize: 16, color: Theme.of(context).primaryColor), 178 | ) 179 | ], 180 | ); 181 | } 182 | 183 | @override 184 | void onLifecycleChanged(LifecycleOwner owner, LifecycleState state) { 185 | if (state == LifecycleState.onCreate) { 186 | ///获取参数 187 | ArticleEntity? entity = RouterHelper.argumentsT(context); 188 | 189 | _buildContext.read().articleEntity = 190 | entity ?? ArticleEntity(); 191 | } 192 | } 193 | } 194 | --------------------------------------------------------------------------------