├── android ├── settings_aar.gradle ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-ldpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── rwson │ │ │ │ │ └── bill │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── icon-29.png │ │ │ ├── icon-40.png │ │ │ ├── icon-50.png │ │ │ ├── icon-57.png │ │ │ ├── icon-72.png │ │ │ ├── icon-76.png │ │ │ ├── icon-1024.png │ │ │ ├── icon-20@2x.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29@2x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-50@2x.png │ │ │ ├── icon-57@2x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-72@2x.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-20-ipad.png │ │ │ ├── icon-29-ipad.png │ │ │ ├── icon-83.5@2x.png │ │ │ ├── icon-20@2x-ipad.png │ │ │ ├── icon-29@2x-ipad.png │ │ │ └── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── 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 │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Podfile.lock └── Podfile ├── lib ├── fonts │ └── font.ttf ├── assets │ ├── avatar.png │ ├── circle.png │ ├── limit.png │ ├── task.png │ └── reminder.png ├── assets.dart ├── bean │ ├── limit.dart │ ├── limit.g.dart │ ├── reminder.dart │ ├── task.dart │ ├── reminder.g.dart │ ├── user.dart │ ├── bill.dart │ ├── task.g.dart │ ├── group.dart │ ├── user.g.dart │ ├── bill.g.dart │ ├── group.g.dart │ ├── statistics.dart │ └── statistics.g.dart ├── stores │ ├── base.dart │ ├── bill.g.dart │ ├── stores.dart │ ├── base.g.dart │ ├── limit.g.dart │ ├── limit.dart │ ├── bill.dart │ ├── task.g.dart │ ├── group.g.dart │ ├── reminder.g.dart │ ├── statistics.dart │ ├── statistics.g.dart │ ├── task.dart │ ├── user.g.dart │ ├── group.dart │ ├── reminder.dart │ └── user.dart ├── pay-channels.dart ├── pages │ ├── bill │ │ ├── detail.dart │ │ └── month-bill-list.dart │ ├── tab │ │ └── analysis │ │ │ └── tooltip.dart │ ├── limit │ │ └── limit-set.dart │ ├── user │ │ ├── login.dart │ │ └── forgot.dart │ ├── group │ │ └── groups.dart │ ├── task │ │ └── tasks.dart │ └── reminder │ │ └── reminders.dart ├── event.dart ├── widgets │ ├── empty.dart │ └── pop-menu.dart ├── router.dart ├── adaptor.dart ├── colors.dart ├── api.dart ├── http │ └── http-util.dart ├── bottom_navigation_widget.dart ├── util.dart ├── methods-icons.dart ├── iconfont.dart └── main.dart ├── screenshoots ├── mine.png ├── index.png ├── wealth.png └── analysis.png ├── .metadata ├── local.properties ├── .vscode └── launch.json ├── README.md ├── .flutter-plugins-dependencies ├── .gitignore └── pubspec.yaml /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /lib/fonts/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/fonts/font.ttf -------------------------------------------------------------------------------- /lib/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/assets/avatar.png -------------------------------------------------------------------------------- /lib/assets/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/assets/circle.png -------------------------------------------------------------------------------- /lib/assets/limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/assets/limit.png -------------------------------------------------------------------------------- /lib/assets/task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/assets/task.png -------------------------------------------------------------------------------- /screenshoots/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/screenshoots/mine.png -------------------------------------------------------------------------------- /lib/assets/reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/lib/assets/reminder.png -------------------------------------------------------------------------------- /screenshoots/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/screenshoots/index.png -------------------------------------------------------------------------------- /screenshoots/wealth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/screenshoots/wealth.png -------------------------------------------------------------------------------- /screenshoots/analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/screenshoots/analysis.png -------------------------------------------------------------------------------- /android/app/src/main/res/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/android/app/src/main/res/ic_launcher.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /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/rwson/bill/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/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/rwson/bill/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/rwson/bill/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/rwson/bill/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rwson/bill/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/assets.dart: -------------------------------------------------------------------------------- 1 | // 静态资源 2 | class Assets { 3 | static String iconAvatar = 'lib/assets/avatar.png'; 4 | 5 | static String iconCircle = 'lib/assets/circle.png'; 6 | 7 | static String iconLimit = 'lib/assets/limit.png'; 8 | 9 | static String iconReminder = 'lib/assets/reminder.png'; 10 | 11 | static String iconTask = 'lib/assets/task.png'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/bean/limit.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'limit.g.dart'; 4 | 5 | @JsonSerializable() 6 | class Limit { 7 | final int limit; 8 | 9 | Limit([this.limit]); 10 | 11 | factory Limit.fromJson(Map json) => _$LimitFromJson(json); 12 | 13 | Map toJson() => _$LimitToJson(this); 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Tue Nov 19 09:20:46 CST 2019 8 | sdk.dir=/Users/rwson/My/sdks/android 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/rwson/bill/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.rwson.bill 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity : FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/stores/base.dart: -------------------------------------------------------------------------------- 1 | import 'package:mobx/mobx.dart'; 2 | 3 | part 'base.g.dart'; 4 | 5 | class BaseStore = _BaseStore with _$BaseStore; 6 | 7 | abstract class _BaseStore with Store { 8 | @observable 9 | bool loading = false; 10 | 11 | @action 12 | bool switchLoading(bool isLoading) { 13 | // if (isLoading) { 14 | // LoadingFlipping.circle(); 15 | // } 16 | if (isLoading != loading) { 17 | loading = isLoading; 18 | } 19 | return loading; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /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/bean/limit.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'limit.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Limit _$LimitFromJson(Map json) { 10 | return Limit( 11 | json['limit'] as int, 12 | ); 13 | } 14 | 15 | Map _$LimitToJson(Limit instance) => { 16 | 'limit': instance.limit, 17 | }; 18 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/bean/reminder.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'reminder.g.dart'; 4 | 5 | @JsonSerializable() 6 | class ReminderItem { 7 | final int id; 8 | final String frequency; 9 | final String time; 10 | final String rule; 11 | final int back; 12 | 13 | ReminderItem(this.id, this.frequency, this.time, this.rule, this.back); 14 | 15 | factory ReminderItem.fromJson(Map json) => 16 | _$ReminderItemFromJson(json); 17 | 18 | Map toJson() => _$ReminderItemToJson(this); 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bill 2 | 3 | 基于Flutter开发的一款记账 4 | 5 | ## 所需依赖 6 | 7 | - fl_chart: Flutter图表 8 | 9 | - flutter_daydart: Flutter端的moment 10 | 11 | - fluro: Flutter页面路由 12 | 13 | - flutter_animation_progress_bar: Flutter进度条 14 | 15 | - flutter_statusbar_manager: Flutter状态栏管理器 16 | 17 | - flutter_swiper: Flutter端的swiper 18 | 19 | - flutter_datetime_picker: Flutter日期选择器 20 | 21 | 22 | 23 | ## 主要界面 24 | 25 | ![index](screenshoots/index.png) 26 | 27 | ![analysis](screenshoots/analysis.png) 28 | 29 | ![wealth](screenshoots/wealth.png) 30 | 31 | ![mine](screenshoots/mine.png) 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/pay-channels.dart: -------------------------------------------------------------------------------- 1 | class ChannelItem { 2 | String type; 3 | String desc; 4 | 5 | ChannelItem({this.type, this.desc}); 6 | } 7 | 8 | List _payChannels = [ 9 | ChannelItem(type: 'pc-1', desc: '支付宝'), 10 | ChannelItem(type: 'pc-2', desc: '微信'), 11 | ChannelItem(type: 'pc-3', desc: '现金'), 12 | ChannelItem(type: 'pc-4', desc: '其他') 13 | ]; 14 | 15 | Map _payChannelMaps = { for (var v in _payChannels) v.type: v }; 16 | 17 | class PayChannels { 18 | static List payChannels = _payChannels; 19 | 20 | static Map payChannelMaps = _payChannelMaps; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"device_info","dependencies":[]},{"name":"flt_telephony_info","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_statusbar_manager","dependencies":[]},{"name":"image_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"mobpush_plugin","dependencies":[]},{"name":"path_provider","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_macos","shared_preferences_web"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]}]} -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_statusbar_manager (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `.symlinks/flutter/ios`) 8 | - flutter_statusbar_manager (from `.symlinks/plugins/flutter_statusbar_manager/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: ".symlinks/flutter/ios" 13 | flutter_statusbar_manager: 14 | :path: ".symlinks/plugins/flutter_statusbar_manager/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 18 | flutter_statusbar_manager: 18588ba7dce7a28a538363d2a9989f9ede4a6710 19 | 20 | PODFILE CHECKSUM: b6a0a141693093b304368d08511b46cf3d1d0ac5 21 | 22 | COCOAPODS: 1.8.4 23 | -------------------------------------------------------------------------------- /lib/pages/bill/detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/colors.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class TaskPage extends StatefulWidget { 6 | @override 7 | State createState() => TaskState(); 8 | } 9 | 10 | class TaskState extends State { 11 | @override 12 | void initState() { 13 | super.initState(); 14 | } 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text('账单详情', 21 | style: TextStyle( 22 | fontSize: Adaptor.px(32.0), 23 | color: AppColors.appTextDark)))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/bean/task.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'task.g.dart'; 4 | 5 | @JsonSerializable() 6 | class TaskItem { 7 | final int id; 8 | final String frequency; 9 | final String time; 10 | final String amount; 11 | final String billType; 12 | final String category; 13 | final String remark; 14 | final String confirm; 15 | final String payMethod; 16 | 17 | TaskItem([this.id, 18 | this.frequency, 19 | this.time, 20 | this.amount, 21 | this.billType, 22 | this.category, 23 | this.remark, 24 | this.confirm, 25 | this.payMethod]); 26 | 27 | factory TaskItem.fromJson(Map json) => _$TaskItemFromJson(json); 28 | 29 | Map toJson() => _$TaskItemToJson(this); 30 | } 31 | -------------------------------------------------------------------------------- /lib/bean/reminder.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'reminder.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ReminderItem _$ReminderItemFromJson(Map json) { 10 | return ReminderItem( 11 | json['id'] as int, 12 | json['frequency'] as String, 13 | json['time'] as String, 14 | json['rule'] as String, 15 | json['back'] as int, 16 | ); 17 | } 18 | 19 | Map _$ReminderItemToJson(ReminderItem instance) => 20 | { 21 | 'id': instance.id, 22 | 'frequency': instance.frequency, 23 | 'time': instance.time, 24 | 'rule': instance.rule, 25 | 'back': instance.back, 26 | }; 27 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/stores/bill.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'bill.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$BillStore on _BillStore, Store { 12 | final _$createBillAsyncAction = AsyncAction('createBill'); 13 | 14 | @override 15 | Future createBill(Map bill) { 16 | return _$createBillAsyncAction.run(() => super.createBill(bill)); 17 | } 18 | 19 | final _$getMonthBillsAsyncAction = AsyncAction('getMonthBills'); 20 | 21 | @override 22 | Future getMonthBills(Map param) { 23 | return _$getMonthBillsAsyncAction.run(() => super.getMonthBills(param)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/event.dart: -------------------------------------------------------------------------------- 1 | typedef void EventCallback(arg); 2 | 3 | class EventBus { 4 | EventBus._internal(); 5 | 6 | //保存单例 7 | static EventBus _singleton = new EventBus._internal(); 8 | 9 | factory EventBus() => _singleton; 10 | var _emap = new Map>(); 11 | 12 | void on(eventName, EventCallback f) { 13 | if (eventName == null || f == null) return; 14 | _emap[eventName] ??= new List(); 15 | _emap[eventName].add(f); 16 | } 17 | 18 | void off(eventName, [EventCallback f]) { 19 | var list = _emap[eventName]; 20 | if (eventName == null || list == null) return; 21 | if (f == null) { 22 | _emap[eventName] = null; 23 | } else { 24 | list.remove(f); 25 | } 26 | } 27 | 28 | void emit(eventName, [arg]) { 29 | var list = _emap[eventName]; 30 | if (list == null) return; 31 | int len = list.length - 1; 32 | for (var i = len; i > -1; --i) { 33 | list[i](arg); 34 | } 35 | } 36 | } 37 | 38 | var AppEvent = new EventBus(); 39 | -------------------------------------------------------------------------------- /lib/bean/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | @JsonSerializable() 6 | class User { 7 | final int id; 8 | final String token; 9 | final String avatar; 10 | final String nickname; 11 | final String lastLogin; 12 | final String lastDevice; 13 | final String mobile; 14 | final String registrationId; 15 | 16 | User( 17 | [this.id, 18 | this.token, 19 | this.avatar, 20 | this.nickname, 21 | this.lastDevice, 22 | this.lastLogin, 23 | this.mobile, 24 | this.registrationId]); 25 | 26 | factory User.fromJson(Map json) => _$UserFromJson(json); 27 | 28 | Map toJson() => _$UserToJson(this); 29 | } 30 | 31 | @JsonSerializable() 32 | class UserBillCount { 33 | final int days; 34 | final int counts; 35 | 36 | UserBillCount([this.days, this.counts]); 37 | 38 | factory UserBillCount.fromJson(Map json) => _$UserBillCountFromJson(json); 39 | 40 | Map toJson() => _$UserBillCountToJson(this); 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/bean/bill.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'bill.g.dart'; 4 | 5 | @JsonSerializable() 6 | class BillRow { 7 | final int id; 8 | final String category; 9 | final String amount; 10 | final String billDate; 11 | final String recordType; 12 | final String payMethod; 13 | final String billType; 14 | final String remark; 15 | 16 | BillRow(this.id, this.category, this.amount, this.billDate, this.billType, this.recordType, this.payMethod, this.remark); 17 | 18 | factory BillRow.fromJson(Map json) => 19 | _$BillRowFromJson(json); 20 | 21 | Map toJson() => _$BillRowToJson(this); 22 | } 23 | 24 | @JsonSerializable() 25 | class BillItem { 26 | final String billDate; 27 | final String totalPay; 28 | final String totalIncome; 29 | final List bills; 30 | 31 | BillItem(this.billDate, this.totalPay, this.totalIncome, this.bills); 32 | 33 | factory BillItem.fromJson(Map json) => 34 | _$BillItemFromJson(json); 35 | 36 | Map toJson() => _$BillItemToJson(this); 37 | } 38 | -------------------------------------------------------------------------------- /lib/widgets/empty.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/colors.dart'; 3 | import 'package:bill/iconfont.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class Empty extends StatelessWidget { 7 | Empty({Key key, this.text, this.child, this.top}) : super(key: key); 8 | 9 | final String text; 10 | 11 | final Widget child; 12 | 13 | final double top; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | padding: EdgeInsets.only(top: top ?? Adaptor.px(300.0)), 19 | child: Center( 20 | child: Column( 21 | children: [ 22 | Icon(IconFont.iconEmpty, size: Adaptor.px(100.0), color: AppColors.appTextLight), 23 | Padding( 24 | padding: EdgeInsets.only(top: Adaptor.px(50.0)), 25 | child: child == null ? Text(text ?? '暂无数据', style: TextStyle( 26 | fontSize: Adaptor.px(28.0), 27 | color: AppColors.appTextLight 28 | )) : child 29 | ) 30 | ] 31 | ) 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/bean/task.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | TaskItem _$TaskItemFromJson(Map json) { 10 | return TaskItem( 11 | json['id'] as int, 12 | json['frequency'] as String, 13 | json['time'] as String, 14 | json['amount'] as String, 15 | json['billType'] as String, 16 | json['category'] as String, 17 | json['remark'] as String, 18 | json['confirm'] as String, 19 | json['payMethod'] as String, 20 | ); 21 | } 22 | 23 | Map _$TaskItemToJson(TaskItem instance) => { 24 | 'id': instance.id, 25 | 'frequency': instance.frequency, 26 | 'time': instance.time, 27 | 'amount': instance.amount, 28 | 'billType': instance.billType, 29 | 'category': instance.category, 30 | 'remark': instance.remark, 31 | 'confirm': instance.confirm, 32 | 'payMethod': instance.payMethod, 33 | }; 34 | -------------------------------------------------------------------------------- /lib/router.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | import 'package:bill/event.dart'; 4 | import 'package:bill/stores/stores.dart'; 5 | import 'package:fluro/fluro.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | const String LOGIN_PATH = 'login'; 9 | 10 | class AppRouter { 11 | static Router router; 12 | 13 | static toPage(BuildContext context, String path, [bool checkLogin = true]) { 14 | final userStore = AppStores.userStore; 15 | 16 | if (checkLogin && userStore.logined != true) { 17 | String finalPath = '$LOGIN_PATH?target=${Uri.encodeComponent(path)}'; 18 | return router.navigateTo(context, finalPath); 19 | } 20 | return router.navigateTo(context, path); 21 | } 22 | 23 | static redirectTo(BuildContext context, String path, 24 | [bool checkLogin = false]) { 25 | Navigator.pop(context); 26 | toPage(context, path, checkLogin); 27 | } 28 | 29 | static back(BuildContext context) { 30 | Navigator.pop(context); 31 | } 32 | 33 | static toHome(BuildContext context) { 34 | AppEvent.emit('switchIndex', 0); 35 | Navigator.popUntil(context, ModalRoute.withName('/')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/bean/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'group.g.dart'; 4 | 5 | @JsonSerializable() 6 | class GroupItemUser { 7 | final int id; 8 | final String nickName; 9 | final String avatar; 10 | 11 | GroupItemUser(this.id, this.nickName, this.avatar); 12 | 13 | factory GroupItemUser.fromJson(Map json) => 14 | _$GroupItemUserFromJson(json); 15 | 16 | Map toJson() => _$GroupItemUserToJson(this); 17 | } 18 | 19 | @JsonSerializable() 20 | class GroupItem { 21 | final int id; 22 | final String name; 23 | final String desc; 24 | final String usage; 25 | final String type; 26 | final int creatorId; 27 | final String isDefault; 28 | final String isPersonal; 29 | final GroupItemUser creator; 30 | final List users; 31 | 32 | GroupItem(this.id, this.name, this.desc, this.usage, this.type, this.creatorId, this.users, this.isDefault, this.isPersonal, this.creator); 33 | 34 | factory GroupItem.fromJson(Map json) => 35 | _$GroupItemFromJson(json); 36 | 37 | Map toJson() => _$GroupItemToJson(this); 38 | } 39 | -------------------------------------------------------------------------------- /lib/adaptor.dart: -------------------------------------------------------------------------------- 1 | // 屏幕适配 2 | 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/material.dart'; 6 | 7 | class Adaptor { 8 | static MediaQueryData mediaQuery = MediaQueryData.fromWindow(window); 9 | static double _width = mediaQuery.size.width; 10 | static double _height = mediaQuery.size.height; 11 | static double _topbarH = mediaQuery.padding.top; 12 | static double _botbarH = mediaQuery.padding.bottom; 13 | static double _pixelRatio = mediaQuery.devicePixelRatio; 14 | static var _ratio; 15 | 16 | static init(int number) { 17 | int uiWidth = number is int ? number : 750; 18 | _ratio = _width / uiWidth; 19 | } 20 | 21 | static double px(double number) { 22 | if (!(_ratio is double || _ratio is int)) { 23 | Adaptor.init(750); 24 | } 25 | double result = number * _ratio; 26 | return result; 27 | } 28 | 29 | static double onePx() { 30 | return 1 / _pixelRatio; 31 | } 32 | 33 | static double screenW() { 34 | return _width; 35 | } 36 | 37 | static double screenH() { 38 | return _height; 39 | } 40 | 41 | static double padTopH() { 42 | return _topbarH; 43 | } 44 | 45 | static double padBotH() { 46 | return _botbarH; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/stores/stores.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/stores/bill.dart'; 2 | import 'package:bill/stores/group.dart'; 3 | import 'package:bill/stores/limit.dart'; 4 | import 'package:bill/stores/reminder.dart'; 5 | import 'package:bill/stores/statistics.dart'; 6 | import 'package:bill/stores/task.dart'; 7 | import 'package:bill/stores/user.dart'; 8 | 9 | BillStore _billStore; 10 | LimitStore _limitStore; 11 | ReminderStore _reminderStore; 12 | TaskStore _taskStore; 13 | UserStore _userStore; 14 | GroupStore _groupStore; 15 | StatisticsStore _statisticsStore; 16 | 17 | class AppStores { 18 | static get userStore => _userStore; 19 | 20 | static get limitStore => _limitStore; 21 | 22 | static get reminderStore => _reminderStore; 23 | 24 | static get taskStore => _taskStore; 25 | 26 | static get billStore => _billStore; 27 | 28 | static get groupStote => _groupStore; 29 | 30 | static get statisticsStore => _statisticsStore; 31 | 32 | static initStores() { 33 | _userStore = new UserStore(); 34 | _limitStore = new LimitStore(); 35 | _taskStore = new TaskStore(); 36 | _reminderStore = new ReminderStore(); 37 | _billStore = new BillStore(); 38 | _groupStore = new GroupStore(); 39 | _statisticsStore = new StatisticsStore(); 40 | 41 | _userStore.ensureLogin(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/stores/base.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'base.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$BaseStore on _BaseStore, Store { 12 | final _$loadingAtom = Atom(name: '_BaseStore.loading'); 13 | 14 | @override 15 | bool get loading { 16 | _$loadingAtom.context.enforceReadPolicy(_$loadingAtom); 17 | _$loadingAtom.reportObserved(); 18 | return super.loading; 19 | } 20 | 21 | @override 22 | set loading(bool value) { 23 | _$loadingAtom.context.conditionallyRunInAction(() { 24 | super.loading = value; 25 | _$loadingAtom.reportChanged(); 26 | }, _$loadingAtom, name: '${_$loadingAtom.name}_set'); 27 | } 28 | 29 | final _$_BaseStoreActionController = ActionController(name: '_BaseStore'); 30 | 31 | @override 32 | bool switchLoading(bool isLoading) { 33 | final _$actionInfo = _$_BaseStoreActionController.startAction(); 34 | try { 35 | return super.switchLoading(isLoading); 36 | } finally { 37 | _$_BaseStoreActionController.endAction(_$actionInfo); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/bean/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) { 10 | return User( 11 | json['id'] as int, 12 | json['token'] as String, 13 | json['avatar'] as String, 14 | json['nickname'] as String, 15 | json['lastDevice'] as String, 16 | json['lastLogin'] as String, 17 | json['mobile'] as String, 18 | json['registrationId'] as String, 19 | ); 20 | } 21 | 22 | Map _$UserToJson(User instance) => { 23 | 'id': instance.id, 24 | 'token': instance.token, 25 | 'avatar': instance.avatar, 26 | 'nickname': instance.nickname, 27 | 'lastLogin': instance.lastLogin, 28 | 'lastDevice': instance.lastDevice, 29 | 'mobile': instance.mobile, 30 | 'registrationId': instance.registrationId, 31 | }; 32 | 33 | UserBillCount _$UserBillCountFromJson(Map json) { 34 | return UserBillCount( 35 | json['days'] as int, 36 | json['counts'] as int, 37 | ); 38 | } 39 | 40 | Map _$UserBillCountToJson(UserBillCount instance) => 41 | { 42 | 'days': instance.days, 43 | 'counts': instance.counts, 44 | }; 45 | -------------------------------------------------------------------------------- /lib/stores/limit.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'limit.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$LimitStore on _LimitStore, Store { 12 | final _$limitAtom = Atom(name: '_LimitStore.limit'); 13 | 14 | @override 15 | int get limit { 16 | _$limitAtom.context.enforceReadPolicy(_$limitAtom); 17 | _$limitAtom.reportObserved(); 18 | return super.limit; 19 | } 20 | 21 | @override 22 | set limit(int value) { 23 | _$limitAtom.context.conditionallyRunInAction(() { 24 | super.limit = value; 25 | _$limitAtom.reportChanged(); 26 | }, _$limitAtom, name: '${_$limitAtom.name}_set'); 27 | } 28 | 29 | final _$setLimitAsyncAction = AsyncAction('setLimit'); 30 | 31 | @override 32 | Future> setLimit(int amount) { 33 | return _$setLimitAsyncAction.run(() => super.setLimit(amount)); 34 | } 35 | 36 | final _$queryLimitAsyncAction = AsyncAction('queryLimit'); 37 | 38 | @override 39 | Future queryLimit([bool toast = false]) { 40 | return _$queryLimitAsyncAction.run(() => super.queryLimit(toast)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/stores/limit.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/http/http-util.dart'; 3 | import 'package:bill/stores/base.dart'; 4 | import 'package:bot_toast/bot_toast.dart'; 5 | import 'package:mobx/mobx.dart'; 6 | 7 | part 'limit.g.dart'; 8 | 9 | class LimitStore = _LimitStore with _$LimitStore; 10 | 11 | abstract class _LimitStore extends BaseStore with Store { 12 | @observable 13 | int limit = 0; 14 | 15 | @action 16 | Future setLimit(int amount) async { 17 | try { 18 | switchLoading(true); 19 | Map resp = await HttpUtil.request( 20 | Api.limitSet, {'limit': amount}, HttpUtil.POST); 21 | switchLoading(false); 22 | 23 | HttpResponse data = new HttpResponse.formJson(resp); 24 | 25 | if (data.success) { 26 | limit = amount; 27 | } 28 | 29 | return data; 30 | } catch (e) {} 31 | } 32 | 33 | @action 34 | Future queryLimit([bool toast = false]) async { 35 | try { 36 | switchLoading(true); 37 | Map resp = 38 | await HttpUtil.request(Api.limitQuery, {}, HttpUtil.GET); 39 | switchLoading(false); 40 | HttpResponse data = new HttpResponse.formJson(resp); 41 | 42 | if (data.success) { 43 | limit = data.data['limit']; 44 | } else { 45 | if (toast) { 46 | BotToast.showText(text: data.message); 47 | } 48 | } 49 | 50 | return data.success; 51 | } catch (e) { 52 | if (toast) { 53 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // ext.kotlin_version = '1.2.71' 3 | ext.kotlin_version = '1.3.0' 4 | repositories { 5 | // google() 6 | // jcenter() 7 | maven { 8 | url 'https://maven.aliyun.com/repository/google' 9 | } 10 | maven { 11 | url 'https://maven.aliyun.com/repository/jcenter' 12 | } 13 | maven { 14 | url 'http://maven.aliyun.com/nexus/content/groups/public' 15 | } 16 | maven { 17 | url 'http://mvn.mob.com/android' 18 | } 19 | } 20 | 21 | dependencies { 22 | // classpath 'com.android.tools.build:gradle:3.2.1' 23 | classpath 'com.android.tools.build:gradle:3.3.2' 24 | classpath 'com.mob.sdk:MobSDK:+' 25 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 26 | } 27 | } 28 | 29 | allprojects { 30 | repositories { 31 | // google() 32 | // jcenter() 33 | maven { 34 | url 'https://maven.aliyun.com/repository/google' 35 | } 36 | maven { 37 | url 'https://maven.aliyun.com/repository/jcenter' 38 | } 39 | maven { 40 | url 'http://maven.aliyun.com/nexus/content/groups/public' 41 | } 42 | } 43 | } 44 | 45 | rootProject.buildDir = '../build' 46 | subprojects { 47 | project.buildDir = "${rootProject.buildDir}/${project.name}" 48 | } 49 | subprojects { 50 | project.evaluationDependsOn(':app') 51 | } 52 | 53 | task clean(type: Delete) { 54 | delete rootProject.buildDir 55 | } 56 | 57 | tasks.withType(JavaCompile) { 58 | options.encoding = "UTF-8" 59 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | 快记账 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/bean/bill.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'bill.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | BillRow _$BillRowFromJson(Map json) { 10 | return BillRow( 11 | json['id'] as int, 12 | json['category'] as String, 13 | json['amount'] as String, 14 | json['billDate'] as String, 15 | json['billType'] as String, 16 | json['recordType'] as String, 17 | json['payMethod'] as String, 18 | json['remark'] as String, 19 | ); 20 | } 21 | 22 | Map _$BillRowToJson(BillRow instance) => { 23 | 'id': instance.id, 24 | 'category': instance.category, 25 | 'amount': instance.amount, 26 | 'billDate': instance.billDate, 27 | 'recordType': instance.recordType, 28 | 'payMethod': instance.payMethod, 29 | 'billType': instance.billType, 30 | 'remark': instance.remark, 31 | }; 32 | 33 | BillItem _$BillItemFromJson(Map json) { 34 | return BillItem( 35 | json['billDate'] as String, 36 | json['totalPay'] as String, 37 | json['totalIncome'] as String, 38 | (json['bills'] as List) 39 | ?.map((e) => 40 | e == null ? null : BillRow.fromJson(e as Map)) 41 | ?.toList(), 42 | ); 43 | } 44 | 45 | Map _$BillItemToJson(BillItem instance) => { 46 | 'billDate': instance.billDate, 47 | 'totalPay': instance.totalPay, 48 | 'totalIncome': instance.totalIncome, 49 | 'bills': instance.bills, 50 | }; 51 | -------------------------------------------------------------------------------- /lib/pages/bill/month-bill-list.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/colors.dart'; 3 | import 'package:bill/stores/reminder.dart'; 4 | import 'package:bill/stores/stores.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class MonthBillListPage extends StatefulWidget { 9 | MonthBillListPage({@required this.month}); 10 | 11 | final String month; 12 | 13 | @override 14 | State createState() => MonthBillListState(); 15 | } 16 | 17 | class MonthBillListState extends State { 18 | final ReminderStore reminderStore = AppStores.reminderStore; 19 | 20 | AppLifecycleState appLifecycleState; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | // print(widget.month); 26 | // reminderStore.getDetail(widget.id); 27 | } 28 | 29 | @override 30 | void deactivate() { 31 | // super.deactivate(); 32 | // bool current = ModalRoute.of(context).isCurrent; 33 | // if (current) { 34 | // reminderStore.getDetail(widget.id); 35 | // } 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | appBar: AppBar( 42 | title: Text('月度账单详情', 43 | style: TextStyle( 44 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 45 | body: Container( 46 | margin: EdgeInsets.all(Adaptor.px(10.0)), 47 | padding: EdgeInsets.only( 48 | left: Adaptor.px(10.0), right: Adaptor.px(10.0)), 49 | decoration: BoxDecoration(color: AppColors.appWhite), 50 | width: Adaptor.px(1060.0), 51 | child: Container() 52 | )); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /lib/bean/group.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'group.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | GroupItemUser _$GroupItemUserFromJson(Map json) { 10 | return GroupItemUser( 11 | json['id'] as int, 12 | json['nickName'] as String, 13 | json['avatar'] as String, 14 | ); 15 | } 16 | 17 | Map _$GroupItemUserToJson(GroupItemUser instance) => 18 | { 19 | 'id': instance.id, 20 | 'nickName': instance.nickName, 21 | 'avatar': instance.avatar, 22 | }; 23 | 24 | GroupItem _$GroupItemFromJson(Map json) { 25 | return GroupItem( 26 | json['id'] as int, 27 | json['name'] as String, 28 | json['desc'] as String, 29 | json['usage'] as String, 30 | json['type'] as String, 31 | json['creatorId'] as int, 32 | (json['users'] as List) 33 | ?.map((e) => e == null 34 | ? null 35 | : GroupItemUser.fromJson(e as Map)) 36 | ?.toList(), 37 | json['isDefault'] as String, 38 | json['isPersonal'] as String, 39 | json['creator'] == null 40 | ? null 41 | : GroupItemUser.fromJson(json['creator'] as Map), 42 | ); 43 | } 44 | 45 | Map _$GroupItemToJson(GroupItem instance) => { 46 | 'id': instance.id, 47 | 'name': instance.name, 48 | 'desc': instance.desc, 49 | 'usage': instance.usage, 50 | 'type': instance.type, 51 | 'creatorId': instance.creatorId, 52 | 'isDefault': instance.isDefault, 53 | 'isPersonal': instance.isPersonal, 54 | 'creator': instance.creator, 55 | 'users': instance.users, 56 | }; 57 | -------------------------------------------------------------------------------- /lib/widgets/pop-menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:positioned_tap_detector/positioned_tap_detector.dart'; 4 | 5 | class PopupMenu extends StatefulWidget { 6 | PopupMenu({@required this.child, @required this.menus, this.callback}); 7 | 8 | Widget child; 9 | List menus; 10 | Function callback; 11 | 12 | @override 13 | State createState() => _PopupMenu(); 14 | } 15 | 16 | class _PopupMenu extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return PositionedTapDetector( 20 | child: widget.child, 21 | onLongPress: (TapPosition position) { 22 | _showMenu(context, position); 23 | }, 24 | ); 25 | } 26 | 27 | PopupMenuButton _popMenu() { 28 | return PopupMenuButton( 29 | itemBuilder: (context) => widget.menus, 30 | onSelected: (dynamic value) { 31 | if (widget.callback != null) { 32 | widget.callback(value); 33 | } 34 | } 35 | ); 36 | } 37 | 38 | _showMenu(BuildContext context, TapPosition position) { 39 | 40 | double lPos = position.relative.dx - 50; 41 | 42 | double tPos = position.global.dy + 20; 43 | 44 | if (tPos >= Adaptor.screenH() - 200) { 45 | tPos = tPos - 140; 46 | } 47 | 48 | PopupMenuButton pop = _popMenu(); 49 | 50 | showMenu( 51 | context: context, 52 | items: pop.itemBuilder(context), 53 | captureInheritedThemes: false, 54 | elevation: 2.0, 55 | position: RelativeRect.fromLTRB(lPos, tPos, 15, 0), 56 | ).then((dynamic newValue) { 57 | if (!mounted) return null; 58 | if (newValue == null) { 59 | if (pop.onCanceled != null) pop.onCanceled(); 60 | return null; 61 | } 62 | if (pop.onSelected != null) pop.onSelected(newValue); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/stores/bill.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/bean/bill.dart'; 3 | import 'package:bill/http/http-util.dart'; 4 | import 'package:bill/stores/base.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'bill.g.dart'; 9 | 10 | class BillStore = _BillStore with _$BillStore; 11 | 12 | abstract class _BillStore extends BaseStore with Store { 13 | List homeBills = []; 14 | 15 | @action 16 | Future createBill(Map bill) async { 17 | try { 18 | switchLoading(true); 19 | Map resp = await HttpUtil.request(Api.createBill, bill, HttpUtil.POST); 20 | switchLoading(false); 21 | 22 | HttpResponse data = new HttpResponse.formJson(resp); 23 | 24 | BotToast.showText(text: data.message); 25 | 26 | return data.success; 27 | } catch (e) { 28 | switchLoading(false); 29 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 30 | return false; 31 | } 32 | } 33 | 34 | 35 | @action 36 | Future getMonthBills(Map param) async { 37 | try { 38 | switchLoading(true); 39 | Map resp = await HttpUtil.request(Api.monthBills, { 40 | 'month': param['month'] 41 | }, HttpUtil.GET); 42 | switchLoading(false); 43 | 44 | HttpResponse data = new HttpResponse.formJson(resp); 45 | 46 | if (data.success) { 47 | homeBills = new List(); 48 | data.data 49 | .toList() 50 | .forEach((json) => {homeBills.add(new BillItem.fromJson(json))}); 51 | } else { 52 | if (param['toast']) { 53 | BotToast.showText(text: data.message); 54 | } 55 | } 56 | } catch (e) { 57 | switchLoading(false); 58 | if (param['toast']) { 59 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 60 | } 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /lib/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const Color appBorder = const Color(0xFFEDEEF3); 5 | static const Color appBorderLight = const Color(0xFFDFDFDF); 6 | static const Color appBorderDark = const Color(0xFF222222); 7 | 8 | static const Color sheetBtnBg = const Color(0xFFF7F7F7); 9 | 10 | static const Color appWhite = const Color(0xFFFFFFFF); 11 | 12 | static const Color appTextNormal = const Color(0xFF666666); 13 | static const Color appTextDark = const Color(0xFF333333); 14 | static const Color appTextLight = const Color(0xFF999999); 15 | 16 | static const Color appIncome = const Color(0xFF03C789); 17 | static const Color appOutlay = const Color(0xFFFF5252); 18 | static const Color appOutlayLight = const Color(0x9FFF5252); 19 | 20 | static const Color appYellow = const Color(0xFFF4B742); 21 | static const Color appYellowLight = const Color(0xFFFED279); 22 | static const Color appYellowShadow = const Color(0x77F4B742); 23 | static const MaterialColor appYellowMaterial = MaterialColor( 24 | 0xFFF4B742, 25 | { 26 | 50: appYellow, 27 | 100: appYellow, 28 | 200: appYellow, 29 | 300: appYellow, 30 | 400: appYellow, 31 | 500: appYellow, 32 | 600: appYellow, 33 | 700: appYellow, 34 | 800: appYellow, 35 | 900: appYellow 36 | }, 37 | ); 38 | 39 | static const Color appGreen = const Color(0xFF03C789); 40 | static const Color appGreenLight = const Color(0xFF47E48B); 41 | static const Color appGreenShadow = const Color(0x7703C789); 42 | static const MaterialColor appGreenMaterial = MaterialColor( 43 | 0xFF03C789, 44 | { 45 | 50: appGreen, 46 | 100: appGreen, 47 | 200: appGreen, 48 | 300: appGreen, 49 | 400: appGreen, 50 | 500: appGreen, 51 | 600: appGreen, 52 | 700: appGreen, 53 | 800: appGreen, 54 | 900: appGreen 55 | }, 56 | ); 57 | 58 | static const Color appBlackShadow = const Color(0x1E000000); 59 | 60 | static const Color appSufficient = const Color(0xFF30F697); 61 | static const Color appRegular = const Color(0xFFFED279); 62 | static const Color appWarning = const Color(0xFFfDA22C); 63 | static const Color appDanger = const Color(0x1FFFF401A); 64 | } 65 | -------------------------------------------------------------------------------- /lib/api.dart: -------------------------------------------------------------------------------- 1 | class Api { 2 | static const String base = 'http://192.168.1.56:3000/'; 3 | 4 | // 用户相关 5 | 6 | static const String registerLogin = 'user/register-login'; 7 | 8 | static const String login = 'user/login'; 9 | 10 | static const String ensureLogined = 'user/ensure-logined'; 11 | 12 | static const String getVCode = 'user/get-vcode'; 13 | 14 | static const String validateVCode = 'user/validate-vcode/:mobile/:code'; 15 | 16 | static const String forgot = 'user/forgot'; 17 | 18 | static const String switchDevice = 'user/switch-device/:registrationId'; 19 | 20 | static const String billCount = 'user/bill-count'; 21 | 22 | // 月度限额相关 23 | 24 | static const String limitSet = 'limit/set'; 25 | 26 | static const String limitQuery = 'limit/query'; 27 | 28 | // 存钱提醒相关 29 | 30 | static const String createReminder = 'reminder/create'; 31 | 32 | static const String updateReminder = 'reminder/update'; 33 | 34 | static const String queryReminder = 'reminder/query'; 35 | 36 | static const String deleteReminder = 'reminder/delete/:id'; 37 | 38 | static const String reminderDetail = 'reminder/detail/:id'; 39 | 40 | // 记账任务相关 41 | 42 | static const String createTask = 'task/create'; 43 | 44 | static const String updateTask = 'task/update'; 45 | 46 | static const String queryTask = 'task/query'; 47 | 48 | static const String deleteTask = 'task/delete/:id'; 49 | 50 | static const String taskDetail = 'task/detail/:id'; 51 | 52 | // 记账圈子相关 53 | 54 | static const String createGroup = 'group/create'; 55 | 56 | static const String updateGroup = 'group/update'; 57 | 58 | static const String queryGroup = 'group/list'; 59 | 60 | static const String deleteGroup = 'group/delete/:id'; 61 | 62 | static const String dispandGroup = 'group/dispand/:id'; 63 | 64 | static const String groupDetail = 'group/detail/:id'; 65 | 66 | // 记账相关 67 | 68 | static const String createBill = 'bill/create'; 69 | 70 | static const String monthBills = 'bill/monthly'; 71 | 72 | // 统计相关 73 | static const String compareLastMonth = 'statistics/compare-lastmonth'; 74 | 75 | static const String billYears = 'statistics/years'; 76 | 77 | static const String monthAnalyze = 'statistics/analyze'; 78 | 79 | static const String yearlyBills = 'statistics/yearly-bills?year=:year'; 80 | 81 | } 82 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 22 | 29 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /lib/stores/task.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'task.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$TaskStore on _TaskStore, Store { 12 | final _$tasksAtom = Atom(name: '_TaskStore.tasks'); 13 | 14 | @override 15 | List get tasks { 16 | _$tasksAtom.context.enforceReadPolicy(_$tasksAtom); 17 | _$tasksAtom.reportObserved(); 18 | return super.tasks; 19 | } 20 | 21 | @override 22 | set tasks(List value) { 23 | _$tasksAtom.context.conditionallyRunInAction(() { 24 | super.tasks = value; 25 | _$tasksAtom.reportChanged(); 26 | }, _$tasksAtom, name: '${_$tasksAtom.name}_set'); 27 | } 28 | 29 | final _$currentAtom = Atom(name: '_TaskStore.current'); 30 | 31 | @override 32 | TaskItem get current { 33 | _$currentAtom.context.enforceReadPolicy(_$currentAtom); 34 | _$currentAtom.reportObserved(); 35 | return super.current; 36 | } 37 | 38 | @override 39 | set current(TaskItem value) { 40 | _$currentAtom.context.conditionallyRunInAction(() { 41 | super.current = value; 42 | _$currentAtom.reportChanged(); 43 | }, _$currentAtom, name: '${_$currentAtom.name}_set'); 44 | } 45 | 46 | final _$createTaskAsyncAction = AsyncAction('createTask'); 47 | 48 | @override 49 | Future createTask(Map task) { 50 | return _$createTaskAsyncAction.run(() => super.createTask(task)); 51 | } 52 | 53 | final _$queryTaskAsyncAction = AsyncAction('queryTask'); 54 | 55 | @override 56 | Future queryTask([bool toast = false]) { 57 | return _$queryTaskAsyncAction.run(() => super.queryTask(toast)); 58 | } 59 | 60 | final _$getDetailAsyncAction = AsyncAction('getDetail'); 61 | 62 | @override 63 | Future getDetail(String id) { 64 | return _$getDetailAsyncAction.run(() => super.getDetail(id)); 65 | } 66 | 67 | final _$deleteTaskAsyncAction = AsyncAction('deleteTask'); 68 | 69 | @override 70 | Future deleteTask(String id) { 71 | return _$deleteTaskAsyncAction.run(() => super.deleteTask(id)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/stores/group.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'group.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$GroupStore on _GroupStore, Store { 12 | final _$groupsAtom = Atom(name: '_GroupStore.groups'); 13 | 14 | @override 15 | List get groups { 16 | _$groupsAtom.context.enforceReadPolicy(_$groupsAtom); 17 | _$groupsAtom.reportObserved(); 18 | return super.groups; 19 | } 20 | 21 | @override 22 | set groups(List value) { 23 | _$groupsAtom.context.conditionallyRunInAction(() { 24 | super.groups = value; 25 | _$groupsAtom.reportChanged(); 26 | }, _$groupsAtom, name: '${_$groupsAtom.name}_set'); 27 | } 28 | 29 | final _$currentAtom = Atom(name: '_GroupStore.current'); 30 | 31 | @override 32 | GroupItem get current { 33 | _$currentAtom.context.enforceReadPolicy(_$currentAtom); 34 | _$currentAtom.reportObserved(); 35 | return super.current; 36 | } 37 | 38 | @override 39 | set current(GroupItem value) { 40 | _$currentAtom.context.conditionallyRunInAction(() { 41 | super.current = value; 42 | _$currentAtom.reportChanged(); 43 | }, _$currentAtom, name: '${_$currentAtom.name}_set'); 44 | } 45 | 46 | final _$createGroupAsyncAction = AsyncAction('createGroup'); 47 | 48 | @override 49 | Future createGroup(Map group) { 50 | return _$createGroupAsyncAction.run(() => super.createGroup(group)); 51 | } 52 | 53 | final _$queryGroupsAsyncAction = AsyncAction('queryGroups'); 54 | 55 | @override 56 | Future queryGroups([bool toast = false]) { 57 | return _$queryGroupsAsyncAction.run(() => super.queryGroups(toast)); 58 | } 59 | 60 | final _$deleteReminderAsyncAction = AsyncAction('deleteReminder'); 61 | 62 | @override 63 | Future deleteReminder(String id) { 64 | return _$deleteReminderAsyncAction.run(() => super.deleteReminder(id)); 65 | } 66 | 67 | final _$getDetailAsyncAction = AsyncAction('getDetail'); 68 | 69 | @override 70 | Future getDetail(String id) { 71 | return _$getDetailAsyncAction.run(() => super.getDetail(id)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /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/stores/reminder.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'reminder.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$ReminderStore on _ReminderStore, Store { 12 | final _$reminderAtom = Atom(name: '_ReminderStore.reminder'); 13 | 14 | @override 15 | List get reminder { 16 | _$reminderAtom.context.enforceReadPolicy(_$reminderAtom); 17 | _$reminderAtom.reportObserved(); 18 | return super.reminder; 19 | } 20 | 21 | @override 22 | set reminder(List value) { 23 | _$reminderAtom.context.conditionallyRunInAction(() { 24 | super.reminder = value; 25 | _$reminderAtom.reportChanged(); 26 | }, _$reminderAtom, name: '${_$reminderAtom.name}_set'); 27 | } 28 | 29 | final _$currentAtom = Atom(name: '_ReminderStore.current'); 30 | 31 | @override 32 | ReminderItem get current { 33 | _$currentAtom.context.enforceReadPolicy(_$currentAtom); 34 | _$currentAtom.reportObserved(); 35 | return super.current; 36 | } 37 | 38 | @override 39 | set current(ReminderItem value) { 40 | _$currentAtom.context.conditionallyRunInAction(() { 41 | super.current = value; 42 | _$currentAtom.reportChanged(); 43 | }, _$currentAtom, name: '${_$currentAtom.name}_set'); 44 | } 45 | 46 | final _$createReminderAsyncAction = AsyncAction('createReminder'); 47 | 48 | @override 49 | Future createReminder(Map reminder) { 50 | return _$createReminderAsyncAction 51 | .run(() => super.createReminder(reminder)); 52 | } 53 | 54 | final _$queryReminderAsyncAction = AsyncAction('queryReminder'); 55 | 56 | @override 57 | Future queryReminder([bool toast = false]) { 58 | return _$queryReminderAsyncAction.run(() => super.queryReminder(toast)); 59 | } 60 | 61 | final _$deleteReminderAsyncAction = AsyncAction('deleteReminder'); 62 | 63 | @override 64 | Future deleteReminder(String id) { 65 | return _$deleteReminderAsyncAction.run(() => super.deleteReminder(id)); 66 | } 67 | 68 | final _$getDetailAsyncAction = AsyncAction('getDetail'); 69 | 70 | @override 71 | Future getDetail(String id) { 72 | return _$getDetailAsyncAction.run(() => super.getDetail(id)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /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 parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | 38 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 39 | # referring to absolute paths on developers' machines. 40 | system('rm -rf .symlinks') 41 | system('mkdir -p .symlinks/plugins') 42 | 43 | # Flutter Pods 44 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 45 | if generated_xcode_build_settings.empty? 46 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first." 47 | end 48 | generated_xcode_build_settings.map { |p| 49 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 50 | symlink = File.join('.symlinks', 'flutter') 51 | File.symlink(File.dirname(p[:path]), symlink) 52 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 53 | end 54 | } 55 | 56 | # Plugin Pods 57 | plugin_pods = parse_KV_file('../.flutter-plugins') 58 | plugin_pods.map { |p| 59 | symlink = File.join('.symlinks', 'plugins', p[:name]) 60 | File.symlink(p[:path], symlink) 61 | pod p[:name], :path => File.join(symlink, 'ios') 62 | } 63 | end 64 | 65 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 66 | install! 'cocoapods', :disable_input_output_paths => true 67 | 68 | post_install do |installer| 69 | installer.pods_project.targets.each do |target| 70 | target.build_configurations.each do |config| 71 | config.build_settings['ENABLE_BITCODE'] = 'NO' 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/http/http-util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bill/api.dart'; 4 | import 'package:bot_toast/bot_toast.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | Future getToken() async { 9 | SharedPreferences prefs = await SharedPreferences.getInstance(); 10 | 11 | String token = prefs.getString('userToken'); 12 | return token; 13 | } 14 | 15 | class HttpResponse { 16 | bool success; 17 | String message; 18 | T data; 19 | 20 | HttpResponse({this.success, this.message, this.data}); 21 | 22 | factory HttpResponse.formJson(json) { 23 | return HttpResponse( 24 | success: json['success'], message: json['message'], data: json['data']); 25 | } 26 | } 27 | 28 | class HttpUtil { 29 | static Dio dio; 30 | 31 | static const String API_PREFIX = Api.base; 32 | static const int CONNECT_TIMEOUT = 10000; 33 | static const int RECEIVE_TIMEOUT = 3000; 34 | 35 | static const String GET = 'get'; 36 | static const String POST = 'post'; 37 | static const String PUT = 'put'; 38 | static const String PATCH = 'patch'; 39 | static const String DELETE = 'delete'; 40 | 41 | static const String UNKNOWN_ERROR = '网络异常~'; 42 | static const String CONNECT_ERROR = '服务器好像开小差了, 等会再试吧~'; 43 | 44 | static Future> request(String url, 45 | [data, method]) async { 46 | data = data ?? {}; 47 | method = method ?? 'GET'; 48 | 49 | final String token = await getToken(); 50 | 51 | Dio dio = createInstance(); 52 | 53 | final Options options = new Options( 54 | method: method, headers: {'authorization': 'Bearer $token'}); 55 | 56 | data.forEach((key, value) { 57 | if (url.indexOf(key) != -1) { 58 | url = url.replaceAll(':$key', value.toString()); 59 | } 60 | }); 61 | 62 | var result; 63 | 64 | try { 65 | Response response = await dio.request(url, data: data, options: options); 66 | result = response.data; 67 | } on DioError catch (e) { 68 | if (e.response != null) { 69 | result = e.response.data; 70 | print(e.response.data); 71 | } else { 72 | print(e); 73 | result = { 74 | 'success': false, 75 | 'message': UNKNOWN_ERROR 76 | }; 77 | BotToast.showText(text: CONNECT_ERROR); 78 | } 79 | } 80 | 81 | return result; 82 | } 83 | 84 | static Dio createInstance() { 85 | if (dio == null) { 86 | BaseOptions options = new BaseOptions( 87 | baseUrl: API_PREFIX, 88 | connectTimeout: CONNECT_TIMEOUT, 89 | receiveTimeout: RECEIVE_TIMEOUT, 90 | ); 91 | 92 | dio = new Dio(options); 93 | } 94 | 95 | return dio; 96 | } 97 | 98 | static clear() { 99 | dio = null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/bottom_navigation_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:bill/adaptor.dart'; 4 | import 'package:bill/event.dart'; 5 | import 'package:bill/iconfont.dart'; 6 | import 'package:bill/pages/tab/analysis/analysis.dart'; 7 | import 'package:bill/pages/tab/index.dart'; 8 | import 'package:bill/pages/tab/mine.dart'; 9 | import 'package:bill/pages/tab/wealth.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class BottomNavigationWidget extends StatefulWidget { 13 | @override 14 | State createState() => BottomNavigationWidgetState(); 15 | } 16 | 17 | class BottomNavigationWidgetState extends State { 18 | int _currentIndex = 0; 19 | List list = List(); 20 | 21 | @override 22 | void initState() { 23 | list 24 | ..add(IndexPage()) 25 | ..add(AnalysisPage()) 26 | ..add(WealthPage()) 27 | ..add(MinePage()); 28 | 29 | AppEvent.on('switchIndex', (dynamic index) { 30 | setState(() { 31 | _currentIndex = index; 32 | }); 33 | }); 34 | 35 | super.initState(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return Scaffold( 41 | body: list[_currentIndex], 42 | bottomNavigationBar: BottomNavigationBar( 43 | items: [ 44 | BottomNavigationBarItem( 45 | icon: Icon( 46 | IconFont.iconIndex, 47 | size: 22.0, 48 | ), 49 | title: Padding( 50 | padding: const EdgeInsets.only(top: 5.0), 51 | child: Text('首页'))), 52 | BottomNavigationBarItem( 53 | icon: Icon(IconFont.iconAnalysis, size: 22.0), 54 | title: Padding( 55 | padding: const EdgeInsets.only(top: 5.0), 56 | child: Text('分析'))), 57 | BottomNavigationBarItem( 58 | icon: Icon(IconFont.iconWealth, size: 22.0), 59 | title: Padding( 60 | padding: const EdgeInsets.only(top: 5.0), 61 | child: Text('资产'))), 62 | BottomNavigationBarItem( 63 | icon: Icon(IconFont.iconMine, size: 22.0), 64 | title: Padding( 65 | padding: const EdgeInsets.only(top: 5.0), 66 | child: Text('我的'))) 67 | ], 68 | currentIndex: _currentIndex, 69 | onTap: (int index) { 70 | setState(() { 71 | _currentIndex = index; 72 | }); 73 | }, 74 | selectedFontSize: Adaptor.px(28.0), 75 | unselectedFontSize: Adaptor.px(28.0), 76 | type: BottomNavigationBarType.fixed, 77 | unselectedItemColor: const Color(0xFFBEBEBE), 78 | selectedItemColor: const Color(0xFFF6C431))); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/stores/statistics.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/bean/statistics.dart'; 3 | import 'package:bill/http/http-util.dart'; 4 | import 'package:bill/stores/base.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'statistics.g.dart'; 9 | 10 | class StatisticsStore = _StatisticsStore with _$StatisticsStore; 11 | 12 | abstract class _StatisticsStore extends BaseStore with Store { 13 | @observable 14 | CompareLastMonth compared; 15 | 16 | @observable 17 | List years; 18 | 19 | @observable 20 | YearlyAnalyze yearlyAnalyze; 21 | 22 | @observable 23 | MonthAnalysis monthAnalysis; 24 | 25 | @action 26 | Future compareLast() async { 27 | try { 28 | switchLoading(true); 29 | 30 | Map resp = 31 | await HttpUtil.request(Api.compareLastMonth, {}, HttpUtil.GET); 32 | 33 | HttpResponse data = new HttpResponse.formJson(resp); 34 | 35 | switchLoading(false); 36 | 37 | if (data.success) { 38 | compared = new CompareLastMonth.fromJson(data.data); 39 | } 40 | 41 | } catch (e) { 42 | compared = null; 43 | switchLoading(false); 44 | } 45 | } 46 | 47 | @action 48 | Future getBillYears() async { 49 | try { 50 | switchLoading(true); 51 | 52 | Map resp = 53 | await HttpUtil.request(Api.billYears, {}, HttpUtil.GET); 54 | 55 | HttpResponse data = new HttpResponse.formJson(resp); 56 | 57 | switchLoading(false); 58 | 59 | if (data.success) { 60 | years = []; 61 | data.data.toList().forEach((year) => {years.add(year as String)}); 62 | } else { 63 | BotToast.showText(text: data.message); 64 | } 65 | 66 | } catch (e) { 67 | years = []; 68 | switchLoading(false); 69 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 70 | } 71 | } 72 | 73 | @action 74 | Future getYearlyBills(Map param) async { 75 | try { 76 | switchLoading(true); 77 | 78 | Map resp = await HttpUtil.request(Api.yearlyBills, param, HttpUtil.GET); 79 | 80 | HttpResponse data = new HttpResponse.formJson(resp); 81 | 82 | switchLoading(false); 83 | 84 | if (data.success) { 85 | yearlyAnalyze = new YearlyAnalyze.fromJson(data.data); 86 | } 87 | 88 | } catch (e) { 89 | switchLoading(false); 90 | } 91 | } 92 | 93 | @action 94 | Future getMonthAnalysis() async { 95 | try { 96 | switchLoading(true); 97 | 98 | Map resp = 99 | await HttpUtil.request(Api.monthAnalyze, {}, HttpUtil.GET); 100 | 101 | HttpResponse data = new HttpResponse.formJson(resp); 102 | 103 | switchLoading(false); 104 | 105 | if (data.success) { 106 | monthAnalysis = new MonthAnalysis.fromJson(data.data); 107 | } 108 | 109 | return data.success; 110 | } catch (e) { 111 | switchLoading(false); 112 | 113 | return false; 114 | } 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /lib/bean/statistics.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'statistics.g.dart'; 4 | 5 | @JsonSerializable() 6 | class CompareLastMonth { 7 | final bool limitSetted; 8 | final int limit; 9 | final double percent; 10 | final double totalPay; 11 | final double totalIncome; 12 | final double totalPayCompare; 13 | final double totalIncomeCompare; 14 | 15 | CompareLastMonth(this.limitSetted, this.limit, this.percent, this.totalPay, this.totalIncome, this.totalPayCompare, this.totalIncomeCompare); 16 | 17 | factory CompareLastMonth.fromJson(Map json) => 18 | _$CompareLastMonthFromJson(json); 19 | 20 | Map toJson() => _$CompareLastMonthToJson(this); 21 | } 22 | 23 | @JsonSerializable() 24 | class YearlyAmount { 25 | final double totalPay; 26 | final double totalIncome; 27 | final double totalRemain; 28 | 29 | YearlyAmount(this.totalPay, this.totalIncome, this.totalRemain); 30 | 31 | factory YearlyAmount.fromJson(Map json) => 32 | _$YearlyAmountFromJson(json); 33 | 34 | Map toJson() => _$YearlyAmountToJson(this); 35 | } 36 | 37 | @JsonSerializable() 38 | class MonthliAmount { 39 | final String billMonth; 40 | final double totalPay; 41 | final double totalIncome; 42 | final double totalRemain; 43 | 44 | MonthliAmount(this.billMonth, this.totalPay, this.totalIncome, this.totalRemain); 45 | 46 | factory MonthliAmount.fromJson(Map json) => 47 | _$MonthliAmountFromJson(json); 48 | 49 | Map toJson() => _$MonthliAmountToJson(this); 50 | } 51 | 52 | @JsonSerializable() 53 | class YearlyAnalyze { 54 | final YearlyAmount yearlyAmounts; 55 | final List monthlyAmounts; 56 | 57 | YearlyAnalyze(this.yearlyAmounts, this.monthlyAmounts); 58 | 59 | factory YearlyAnalyze.fromJson(Map json) => 60 | _$YearlyAnalyzeFromJson(json); 61 | 62 | Map toJson() => _$YearlyAnalyzeToJson(this); 63 | } 64 | 65 | @JsonSerializable() 66 | class BillAmountsRow { 67 | final double totalAmount; 68 | final String category; 69 | 70 | BillAmountsRow(this.totalAmount, this.category); 71 | 72 | factory BillAmountsRow.fromJson(Map json) => 73 | _$BillAmountsRowFromJson(json); 74 | 75 | Map toJson() => _$BillAmountsRowToJson(this); 76 | } 77 | 78 | @JsonSerializable() 79 | class DayAmountsRow { 80 | final double totalAmount; 81 | final String billDate; 82 | 83 | DayAmountsRow(this.totalAmount, this.billDate); 84 | 85 | factory DayAmountsRow.fromJson(Map json) => 86 | _$DayAmountsRowFromJson(json); 87 | 88 | Map toJson() => _$DayAmountsRowToJson(this); 89 | } 90 | 91 | @JsonSerializable() 92 | class MonthAnalysis { 93 | // final List monthRanks; 94 | final List monthDayAmount; 95 | 96 | MonthAnalysis(this.monthDayAmount); 97 | 98 | factory MonthAnalysis.fromJson(Map json) => 99 | _$MonthAnalysisFromJson(json); 100 | 101 | Map toJson() => _$MonthAnalysisToJson(this); 102 | } 103 | -------------------------------------------------------------------------------- /lib/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_daydart/flutter_daydart.dart'; 6 | 7 | RegExp mobileReg = new RegExp( 8 | '^((13[0-9])|(15[^4])|(166)|(17[0-8])|(18[0-9])|(19[8-9])|(147,145))\\d{8}\$'); 9 | 10 | class PrecisionLimitFormatter extends TextInputFormatter { 11 | int _scale; 12 | 13 | PrecisionLimitFormatter(this._scale); 14 | 15 | RegExp exp = new RegExp("[0-9.]"); 16 | static const String POINTER = "."; 17 | static const String DOUBLE_ZERO = "00"; 18 | 19 | @override 20 | TextEditingValue formatEditUpdate( 21 | TextEditingValue oldValue, TextEditingValue newValue) { 22 | if (newValue.text.isEmpty) { 23 | return TextEditingValue(); 24 | } 25 | 26 | if (!exp.hasMatch(newValue.text)) { 27 | return oldValue; 28 | } 29 | 30 | if (newValue.text.contains(POINTER)) { 31 | if (newValue.text.indexOf(POINTER) != 32 | newValue.text.lastIndexOf(POINTER)) { 33 | return oldValue; 34 | } 35 | String input = newValue.text; 36 | int index = input.indexOf(POINTER); 37 | 38 | int lengthAfterPointer = input.substring(index, input.length).length - 1; 39 | if (lengthAfterPointer > _scale) { 40 | return oldValue; 41 | } 42 | } else if (newValue.text.startsWith(POINTER) || 43 | newValue.text.startsWith(DOUBLE_ZERO)) { 44 | return oldValue; 45 | } 46 | return newValue; 47 | } 48 | } 49 | 50 | class Util { 51 | static String frenquency2Weekday(String frequency) { 52 | List frequencys = frequency.split('-'); 53 | List weekdays = new List(); 54 | 55 | if (frequencys.length == 7) { 56 | return '每天'; 57 | } 58 | 59 | frequencys.forEach((String item) { 60 | switch (item) { 61 | case '1': 62 | weekdays.add('周一'); 63 | break; 64 | 65 | case '2': 66 | weekdays.add('周二'); 67 | break; 68 | 69 | case '3': 70 | weekdays.add('周三'); 71 | break; 72 | 73 | case '4': 74 | weekdays.add('周四'); 75 | break; 76 | 77 | case '5': 78 | weekdays.add('周五'); 79 | break; 80 | 81 | case '6': 82 | weekdays.add('周六'); 83 | break; 84 | 85 | default: 86 | weekdays.add('周日'); 87 | break; 88 | } 89 | }); 90 | 91 | return weekdays.join(' '); 92 | } 93 | 94 | static bool isAfter(String time) { 95 | int now = int.parse(DayDart().format(fm: 'HH:mm').replaceAll(':', '')); 96 | int times = int.parse(time.replaceAll(':', '')); 97 | return now > times; 98 | } 99 | 100 | static bool validMobile(String mobile) { 101 | return mobileReg.hasMatch(mobile); 102 | } 103 | 104 | static String randomStr(int len) { 105 | String alphabet = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'; 106 | String res = ''; 107 | for (int i = 0; i < len; i++) { 108 | res = '${res}${alphabet[Random().nextInt(alphabet.length)]}'; 109 | } 110 | 111 | return res; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /lib/pages/tab/analysis/tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:bill/adaptor.dart'; 4 | 5 | import 'package:charts_flutter/flutter.dart'; 6 | import 'package:charts_flutter/src/text_element.dart' as ChartText; 7 | import 'package:charts_flutter/src/text_style.dart' as ChartStyle; 8 | 9 | import 'package:flutter/material.dart'; 10 | 11 | String _title; 12 | 13 | String _subTitle; 14 | 15 | class ToolTipMgr { 16 | static String get title => _title; 17 | 18 | static String get subTitle => _subTitle; 19 | 20 | static setTitle(Map data) { 21 | if (data['title'] != null && data['title'].length > 0) { 22 | _title = data['title']; 23 | } 24 | 25 | if (data['subTitle'] != null && data['subTitle'].length > 0) { 26 | _subTitle = data['subTitle']; 27 | } 28 | } 29 | } 30 | 31 | class CustomCircleSymbolRenderer extends CircleSymbolRenderer { 32 | double maxLeft = Adaptor.px(1060); 33 | 34 | @override 35 | void paint(ChartCanvas canvas, Rectangle bounds, 36 | {List dashPattern, 37 | Color fillColor, 38 | FillPatternType fillPattern, 39 | Color strokeColor, 40 | double strokeWidthPx}) { 41 | super.paint(canvas, bounds, 42 | dashPattern: dashPattern, 43 | fillColor: fillColor, 44 | strokeColor: strokeColor, 45 | strokeWidthPx: strokeWidthPx); 46 | 47 | String title = ToolTipMgr.title; 48 | 49 | String subTitle = ToolTipMgr.subTitle; 50 | 51 | // print(title); 52 | // print(subTitle); 53 | 54 | if (title != null && title.length > 0 && subTitle != null && subTitle.length > 0) { 55 | 56 | 57 | double top = -4; 58 | int titleFontSize = Adaptor.px(24).round(); 59 | int subTitleFontSize = Adaptor.px(18).round(); 60 | int titleWidth = (title.length * titleFontSize / 2).round(); 61 | int subTitleWidth = (subTitle.length * subTitleFontSize / 2).round(); 62 | 63 | int maxWidth = titleWidth; 64 | int left = bounds.left.ceil(); 65 | 66 | int _titleLeft; 67 | int _subTitleLeft; 68 | 69 | if (subTitleWidth > maxWidth) { 70 | maxWidth = subTitleWidth; 71 | } 72 | 73 | _titleLeft = left + (((maxWidth - titleWidth) / 2)).round(); 74 | _subTitleLeft = left + (((maxWidth - subTitleWidth) / 2)).round(); 75 | 76 | canvas.drawRRect( 77 | Rectangle(left, top, maxWidth + 10, 26), 78 | fill: MaterialPalette.blue.shadeDefault, 79 | radius: Adaptor.px(6.0), 80 | roundTopLeft: true, 81 | roundBottomLeft: true, 82 | roundTopRight: true, 83 | roundBottomRight: true 84 | ); 85 | 86 | ChartStyle.TextStyle titleTextStyle = ChartStyle.TextStyle(); 87 | titleTextStyle.color = Color.white; 88 | titleTextStyle.fontSize = Adaptor.px(24).toInt(); 89 | canvas.drawText(ChartText.TextElement(title, style: titleTextStyle), _titleLeft + 8, (top + 3).round()); 90 | 91 | ChartStyle.TextStyle subTitleTextStyle = ChartStyle.TextStyle(); 92 | subTitleTextStyle.color = Color.white; 93 | subTitleTextStyle.fontSize = Adaptor.px(18).toInt(); 94 | canvas.drawText(ChartText.TextElement(subTitle, style: subTitleTextStyle), _subTitleLeft, (top + 15).round()); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "size": "20x20", 5 | "idiom": "iphone", 6 | "filename": "icon-20@2x.png", 7 | "scale": "2x" 8 | }, 9 | { 10 | "size": "20x20", 11 | "idiom": "iphone", 12 | "filename": "icon-20@3x.png", 13 | "scale": "3x" 14 | }, 15 | { 16 | "size": "29x29", 17 | "idiom": "iphone", 18 | "filename": "icon-29.png", 19 | "scale": "1x" 20 | }, 21 | { 22 | "size": "29x29", 23 | "idiom": "iphone", 24 | "filename": "icon-29@2x.png", 25 | "scale": "2x" 26 | }, 27 | { 28 | "size": "29x29", 29 | "idiom": "iphone", 30 | "filename": "icon-29@3x.png", 31 | "scale": "3x" 32 | }, 33 | { 34 | "size": "40x40", 35 | "idiom": "iphone", 36 | "filename": "icon-40@2x.png", 37 | "scale": "2x" 38 | }, 39 | { 40 | "size": "40x40", 41 | "idiom": "iphone", 42 | "filename": "icon-40@3x.png", 43 | "scale": "3x" 44 | }, 45 | { 46 | "size": "60x60", 47 | "idiom": "iphone", 48 | "filename": "icon-60@2x.png", 49 | "scale": "2x" 50 | }, 51 | { 52 | "size": "60x60", 53 | "idiom": "iphone", 54 | "filename": "icon-60@3x.png", 55 | "scale": "3x" 56 | }, 57 | { 58 | "size": "20x20", 59 | "idiom": "ipad", 60 | "filename": "icon-20-ipad.png", 61 | "scale": "1x" 62 | }, 63 | { 64 | "size": "20x20", 65 | "idiom": "ipad", 66 | "filename": "icon-20@2x-ipad.png", 67 | "scale": "2x" 68 | }, 69 | { 70 | "size": "29x29", 71 | "idiom": "ipad", 72 | "filename": "icon-29-ipad.png", 73 | "scale": "1x" 74 | }, 75 | { 76 | "size": "29x29", 77 | "idiom": "ipad", 78 | "filename": "icon-29@2x-ipad.png", 79 | "scale": "2x" 80 | }, 81 | { 82 | "size": "40x40", 83 | "idiom": "ipad", 84 | "filename": "icon-40.png", 85 | "scale": "1x" 86 | }, 87 | { 88 | "size": "40x40", 89 | "idiom": "ipad", 90 | "filename": "icon-40@2x.png", 91 | "scale": "2x" 92 | }, 93 | { 94 | "size": "76x76", 95 | "idiom": "ipad", 96 | "filename": "icon-76.png", 97 | "scale": "1x" 98 | }, 99 | { 100 | "size": "76x76", 101 | "idiom": "ipad", 102 | "filename": "icon-76@2x.png", 103 | "scale": "2x" 104 | }, 105 | { 106 | "size": "83.5x83.5", 107 | "idiom": "ipad", 108 | "filename": "icon-83.5@2x.png", 109 | "scale": "2x" 110 | }, 111 | { 112 | "size": "1024x1024", 113 | "idiom": "ios-marketing", 114 | "filename": "icon-1024.png", 115 | "scale": "1x" 116 | } 117 | ], 118 | "info": { 119 | "version": 1, 120 | "author": "icon.wuruihong.com" 121 | } 122 | } -------------------------------------------------------------------------------- /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 | apply plugin: 'com.mob.sdk' 29 | 30 | android { 31 | compileSdkVersion 28 32 | 33 | sourceSets { 34 | main.java.srcDirs += 'src/main/kotlin' 35 | } 36 | 37 | lintOptions { 38 | disable 'InvalidPackage' 39 | } 40 | 41 | defaultConfig { 42 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 43 | applicationId "com.rwson.bill" 44 | minSdkVersion 18 45 | targetSdkVersion 28 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 49 | multiDexEnabled true 50 | } 51 | 52 | buildTypes { 53 | release { 54 | // TODO: Add your own signing config for the release build. 55 | // Signing with the debug keys for now, so `flutter run --release` works. 56 | signingConfig signingConfigs.debug 57 | } 58 | } 59 | } 60 | 61 | flutter { 62 | source '../..' 63 | } 64 | 65 | dependencies { 66 | implementation 'androidx.appcompat:appcompat:1.0.0' 67 | implementation 'androidx.multidex:multidex:2.0.1' 68 | implementation 'com.android.support:support-v13:26.0.2' 69 | implementation 'com.github.wangshihu123:DaemonLibrary:v1.2.1' 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | testImplementation 'junit:junit:4.12' 72 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 73 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 74 | } 75 | 76 | MobSDK { 77 | appKey "2da9fee4f39a2" 78 | appSecret "1f861f4ae247be063077ceaf17416b98" 79 | 80 | MobPush { 81 | // devInfo { 82 | // XIAOMI { 83 | // appId "您的小米平台appId" 84 | // appKey "您的小米平台appKey" 85 | // } 86 | // 87 | // HUAWEI { 88 | // appId "您的华为平台appId" 89 | // } 90 | // 91 | // MEIZU { 92 | // appId "您的魅族平台appId" 93 | // appKey "您的魅族平台appKey" 94 | // } 95 | // 96 | // FCM { 97 | // iconRes "@mipmap/default_ic_launcher" 98 | // } 99 | // 100 | // OPPO { 101 | // appKey "您的OPPO平台appKey" 102 | // appSecret "您的OPPO平台appSecret" 103 | // } 104 | // 105 | // VIVO { 106 | // appId "您的VIVO平台appId" 107 | // appKey "您的VIVO平台appKey" 108 | // } 109 | // } 110 | } 111 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/stores/statistics.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'statistics.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$StatisticsStore on _StatisticsStore, Store { 12 | final _$comparedAtom = Atom(name: '_StatisticsStore.compared'); 13 | 14 | @override 15 | CompareLastMonth get compared { 16 | _$comparedAtom.context.enforceReadPolicy(_$comparedAtom); 17 | _$comparedAtom.reportObserved(); 18 | return super.compared; 19 | } 20 | 21 | @override 22 | set compared(CompareLastMonth value) { 23 | _$comparedAtom.context.conditionallyRunInAction(() { 24 | super.compared = value; 25 | _$comparedAtom.reportChanged(); 26 | }, _$comparedAtom, name: '${_$comparedAtom.name}_set'); 27 | } 28 | 29 | final _$yearsAtom = Atom(name: '_StatisticsStore.years'); 30 | 31 | @override 32 | List get years { 33 | _$yearsAtom.context.enforceReadPolicy(_$yearsAtom); 34 | _$yearsAtom.reportObserved(); 35 | return super.years; 36 | } 37 | 38 | @override 39 | set years(List value) { 40 | _$yearsAtom.context.conditionallyRunInAction(() { 41 | super.years = value; 42 | _$yearsAtom.reportChanged(); 43 | }, _$yearsAtom, name: '${_$yearsAtom.name}_set'); 44 | } 45 | 46 | final _$yearlyAnalyzeAtom = Atom(name: '_StatisticsStore.yearlyAnalyze'); 47 | 48 | @override 49 | YearlyAnalyze get yearlyAnalyze { 50 | _$yearlyAnalyzeAtom.context.enforceReadPolicy(_$yearlyAnalyzeAtom); 51 | _$yearlyAnalyzeAtom.reportObserved(); 52 | return super.yearlyAnalyze; 53 | } 54 | 55 | @override 56 | set yearlyAnalyze(YearlyAnalyze value) { 57 | _$yearlyAnalyzeAtom.context.conditionallyRunInAction(() { 58 | super.yearlyAnalyze = value; 59 | _$yearlyAnalyzeAtom.reportChanged(); 60 | }, _$yearlyAnalyzeAtom, name: '${_$yearlyAnalyzeAtom.name}_set'); 61 | } 62 | 63 | final _$monthAnalysisAtom = Atom(name: '_StatisticsStore.monthAnalysis'); 64 | 65 | @override 66 | MonthAnalysis get monthAnalysis { 67 | _$monthAnalysisAtom.context.enforceReadPolicy(_$monthAnalysisAtom); 68 | _$monthAnalysisAtom.reportObserved(); 69 | return super.monthAnalysis; 70 | } 71 | 72 | @override 73 | set monthAnalysis(MonthAnalysis value) { 74 | _$monthAnalysisAtom.context.conditionallyRunInAction(() { 75 | super.monthAnalysis = value; 76 | _$monthAnalysisAtom.reportChanged(); 77 | }, _$monthAnalysisAtom, name: '${_$monthAnalysisAtom.name}_set'); 78 | } 79 | 80 | final _$compareLastAsyncAction = AsyncAction('compareLast'); 81 | 82 | @override 83 | Future compareLast() { 84 | return _$compareLastAsyncAction.run(() => super.compareLast()); 85 | } 86 | 87 | final _$getBillYearsAsyncAction = AsyncAction('getBillYears'); 88 | 89 | @override 90 | Future getBillYears() { 91 | return _$getBillYearsAsyncAction.run(() => super.getBillYears()); 92 | } 93 | 94 | final _$getYearlyBillsAsyncAction = AsyncAction('getYearlyBills'); 95 | 96 | @override 97 | Future getYearlyBills(Map param) { 98 | return _$getYearlyBillsAsyncAction.run(() => super.getYearlyBills(param)); 99 | } 100 | 101 | final _$getMonthAnalysisAsyncAction = AsyncAction('getMonthAnalysis'); 102 | 103 | @override 104 | Future getMonthAnalysis() { 105 | return _$getMonthAnalysisAsyncAction.run(() => super.getMonthAnalysis()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/stores/task.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/bean/task.dart'; 3 | import 'package:bill/http/http-util.dart'; 4 | import 'package:bill/stores/base.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'task.g.dart'; 9 | 10 | class TaskStore = _TaskStore with _$TaskStore; 11 | 12 | abstract class _TaskStore extends BaseStore with Store { 13 | @observable 14 | List tasks; 15 | 16 | @observable 17 | TaskItem current; 18 | 19 | @action 20 | Future createTask(Map task) async { 21 | try { 22 | switchLoading(true); 23 | 24 | Map resp = 25 | await HttpUtil.request(Api.createTask, task, HttpUtil.POST); 26 | HttpResponse data = new HttpResponse.formJson(resp); 27 | 28 | switchLoading(false); 29 | 30 | BotToast.showText(text: data.message); 31 | 32 | return data.success; 33 | } catch (e) { 34 | switchLoading(false); 35 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 36 | return false; 37 | } 38 | } 39 | 40 | Future updateTask(Map task) async { 41 | try { 42 | switchLoading(true); 43 | Map resp = 44 | await HttpUtil.request(Api.updateTask, task, HttpUtil.PUT); 45 | 46 | HttpResponse data = new HttpResponse.formJson(resp); 47 | 48 | switchLoading(false); 49 | 50 | BotToast.showText(text: data.message); 51 | 52 | return data.success; 53 | } catch (e) { 54 | switchLoading(false); 55 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 56 | return false; 57 | } 58 | } 59 | 60 | @action 61 | Future queryTask([bool toast = false]) async { 62 | try { 63 | switchLoading(true); 64 | 65 | Map resp = 66 | await HttpUtil.request(Api.queryTask, {}, HttpUtil.GET); 67 | 68 | HttpResponse data = new HttpResponse.formJson(resp); 69 | 70 | if (data.success) { 71 | tasks = new List(); 72 | data.data 73 | .toList() 74 | .forEach((json) => {tasks.add(new TaskItem.fromJson(json))}); 75 | } 76 | 77 | switchLoading(false); 78 | 79 | if (toast) { 80 | BotToast.showText(text: data.message); 81 | } 82 | return data.success; 83 | } catch (e) { 84 | switchLoading(false); 85 | if (toast) { 86 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 87 | } 88 | return false; 89 | } 90 | } 91 | 92 | @action 93 | Future getDetail(String id) async { 94 | try { 95 | switchLoading(true); 96 | 97 | Map resp = 98 | await HttpUtil.request(Api.taskDetail, {'id': id}, HttpUtil.GET); 99 | 100 | HttpResponse data = new HttpResponse.formJson(resp); 101 | 102 | switchLoading(false); 103 | 104 | if (data.success) { 105 | current = new TaskItem.fromJson(data.data); 106 | } else { 107 | BotToast.showText(text: data.message); 108 | } 109 | 110 | return data.success; 111 | } catch (e) { 112 | current = null; 113 | switchLoading(false); 114 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 115 | return false; 116 | } 117 | } 118 | 119 | @action 120 | Future deleteTask(String id) async { 121 | try { 122 | switchLoading(true); 123 | 124 | Map resp = 125 | await HttpUtil.request(Api.deleteTask, { 126 | 'id': id 127 | }, HttpUtil.DELETE); 128 | 129 | HttpResponse data = new HttpResponse.formJson(resp); 130 | 131 | switchLoading(false); 132 | 133 | BotToast.showText(text: data.message); 134 | return data.success; 135 | } catch (e) { 136 | switchLoading(false); 137 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 138 | return false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/stores/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // StoreGenerator 7 | // ************************************************************************** 8 | 9 | // ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic 10 | 11 | mixin _$UserStore on _UserStore, Store { 12 | final _$loginedAtom = Atom(name: '_UserStore.logined'); 13 | 14 | @override 15 | bool get logined { 16 | _$loginedAtom.context.enforceReadPolicy(_$loginedAtom); 17 | _$loginedAtom.reportObserved(); 18 | return super.logined; 19 | } 20 | 21 | @override 22 | set logined(bool value) { 23 | _$loginedAtom.context.conditionallyRunInAction(() { 24 | super.logined = value; 25 | _$loginedAtom.reportChanged(); 26 | }, _$loginedAtom, name: '${_$loginedAtom.name}_set'); 27 | } 28 | 29 | final _$userInfoAtom = Atom(name: '_UserStore.userInfo'); 30 | 31 | @override 32 | User get userInfo { 33 | _$userInfoAtom.context.enforceReadPolicy(_$userInfoAtom); 34 | _$userInfoAtom.reportObserved(); 35 | return super.userInfo; 36 | } 37 | 38 | @override 39 | set userInfo(User value) { 40 | _$userInfoAtom.context.conditionallyRunInAction(() { 41 | super.userInfo = value; 42 | _$userInfoAtom.reportChanged(); 43 | }, _$userInfoAtom, name: '${_$userInfoAtom.name}_set'); 44 | } 45 | 46 | final _$billCountAtom = Atom(name: '_UserStore.billCount'); 47 | 48 | @override 49 | UserBillCount get billCount { 50 | _$billCountAtom.context.enforceReadPolicy(_$billCountAtom); 51 | _$billCountAtom.reportObserved(); 52 | return super.billCount; 53 | } 54 | 55 | @override 56 | set billCount(UserBillCount value) { 57 | _$billCountAtom.context.conditionallyRunInAction(() { 58 | super.billCount = value; 59 | _$billCountAtom.reportChanged(); 60 | }, _$billCountAtom, name: '${_$billCountAtom.name}_set'); 61 | } 62 | 63 | final _$ensureLoginAsyncAction = AsyncAction('ensureLogin'); 64 | 65 | @override 66 | Future ensureLogin() { 67 | return _$ensureLoginAsyncAction.run(() => super.ensureLogin()); 68 | } 69 | 70 | final _$loginAsyncAction = AsyncAction('login'); 71 | 72 | @override 73 | Future login(String mobile, String password, [String device]) { 74 | return _$loginAsyncAction.run(() => super.login(mobile, password, device)); 75 | } 76 | 77 | final _$validateCodeAsyncAction = AsyncAction('validateCode'); 78 | 79 | @override 80 | Future validateCode(String mobile, String code) { 81 | return _$validateCodeAsyncAction 82 | .run(() => super.validateCode(mobile, code)); 83 | } 84 | 85 | final _$registerLoginAsyncAction = AsyncAction('registerLogin'); 86 | 87 | @override 88 | Future registerLogin(String mobile, String password, String rid, 89 | [String device]) { 90 | return _$registerLoginAsyncAction 91 | .run(() => super.registerLogin(mobile, password, rid, device)); 92 | } 93 | 94 | final _$forgotAsyncAction = AsyncAction('forgot'); 95 | 96 | @override 97 | Future forgot(String mobile, String password) { 98 | return _$forgotAsyncAction.run(() => super.forgot(mobile, password)); 99 | } 100 | 101 | final _$getBillCountAsyncAction = AsyncAction('getBillCount'); 102 | 103 | @override 104 | Future getBillCount() { 105 | return _$getBillCountAsyncAction.run(() => super.getBillCount()); 106 | } 107 | 108 | final _$_UserStoreActionController = ActionController(name: '_UserStore'); 109 | 110 | @override 111 | bool logout() { 112 | final _$actionInfo = _$_UserStoreActionController.startAction(); 113 | try { 114 | return super.logout(); 115 | } finally { 116 | _$_UserStoreActionController.endAction(_$actionInfo); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/stores/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/bean/group.dart'; 3 | import 'package:bill/http/http-util.dart'; 4 | import 'package:bill/stores/base.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'group.g.dart'; 9 | 10 | class GroupStore = _GroupStore with _$GroupStore; 11 | 12 | abstract class _GroupStore extends BaseStore with Store { 13 | @observable 14 | List groups = []; 15 | 16 | @observable 17 | GroupItem current; 18 | 19 | @action 20 | Future createGroup(Map group) async { 21 | try { 22 | switchLoading(true); 23 | 24 | Map resp = 25 | await HttpUtil.request(Api.createGroup, group, HttpUtil.POST); 26 | HttpResponse data = new HttpResponse.formJson(resp); 27 | 28 | switchLoading(false); 29 | 30 | BotToast.showText(text: data.message); 31 | 32 | return data.success; 33 | } catch (e) { 34 | switchLoading(false); 35 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 36 | return false; 37 | } 38 | } 39 | 40 | Future editGroup(Map group) async { 41 | try { 42 | switchLoading(true); 43 | Map resp = 44 | await HttpUtil.request(Api.updateGroup, group, HttpUtil.PUT); 45 | 46 | HttpResponse data = new HttpResponse.formJson(resp); 47 | 48 | switchLoading(false); 49 | 50 | BotToast.showText(text: data.message); 51 | 52 | return data.success; 53 | } catch (e) { 54 | switchLoading(false); 55 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 56 | return false; 57 | } 58 | } 59 | 60 | @action 61 | Future queryGroups([bool toast = false]) async { 62 | try { 63 | switchLoading(true); 64 | 65 | Map resp = 66 | await HttpUtil.request(Api.queryGroup, {}, HttpUtil.GET); 67 | 68 | HttpResponse data = new HttpResponse.formJson(resp); 69 | 70 | if (data.success) { 71 | groups = new List(); 72 | data.data 73 | .toList() 74 | .forEach((json) => {groups.add(new GroupItem.fromJson(json))}); 75 | } 76 | 77 | switchLoading(false); 78 | 79 | if (toast) { 80 | BotToast.showText(text: data.message); 81 | } 82 | return data.success; 83 | } catch (e) { 84 | switchLoading(false); 85 | if (toast) { 86 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 87 | } 88 | return false; 89 | } 90 | } 91 | 92 | @action 93 | Future deleteReminder(String id) async { 94 | try { 95 | switchLoading(true); 96 | 97 | Map resp = 98 | await HttpUtil.request(Api.deleteReminder, { 99 | 'id': id 100 | }, HttpUtil.DELETE); 101 | 102 | HttpResponse data = new HttpResponse.formJson(resp); 103 | 104 | switchLoading(false); 105 | 106 | BotToast.showText(text: data.message); 107 | return data.success; 108 | } catch (e) { 109 | switchLoading(false); 110 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 111 | return false; 112 | } 113 | } 114 | 115 | @action 116 | Future getDetail(String id) async { 117 | try { 118 | switchLoading(true); 119 | 120 | Map resp = 121 | await HttpUtil.request(Api.groupDetail, {'id': id}, HttpUtil.GET); 122 | 123 | HttpResponse data = new HttpResponse.formJson(resp); 124 | 125 | switchLoading(false); 126 | 127 | if (data.success) { 128 | current = new GroupItem.fromJson(data.data); 129 | } else { 130 | BotToast.showText(text: data.message); 131 | } 132 | 133 | return data.success; 134 | } catch (e) { 135 | current = null; 136 | switchLoading(false); 137 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 138 | return false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bill 2 | description: a bill record application build with flutter by rwson 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.3.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | flutter_localizations: 23 | sdk: flutter 24 | fl_chart: 0.8.0 25 | flutter_daydart: 0.0.3 26 | fluro: 1.5.1 27 | percent_indicator: 2.1.1+1 28 | flutter_swiper: 1.1.6 29 | dio: 3.0.8 30 | shared_preferences: 0.5.6+1 31 | mobx: 1.0.1 32 | flutter_mobx: 1.0.1 33 | json_annotation: 3.0.1 34 | splashscreen: 1.2.0 35 | bot_toast: 2.2.1 36 | flutter_cupertino_localizations: 1.0.1 37 | device_info: 0.4.1+4 38 | flt_telephony_info: 0.1.3 39 | mobpush_plugin: 1.0.9 40 | path_provider: 1.6.0 41 | flutter_rounded_date_picker: 1.0.3 42 | image_picker: 0.6.3+1 43 | loading_animations: 2.1.0 44 | positioned_tap_detector: 1.0.3 45 | charts_flutter: 0.9.0 46 | flutter_statusbar_manager: 47 | git: https://github.com/guitcastro/flutter_statusbar_manager.git 48 | 49 | dev_dependencies: 50 | flutter_test: 51 | sdk: flutter 52 | build_runner: 1.7.3 53 | mobx_codegen: 1.0.1 54 | json_serializable: 3.2.5 55 | 56 | # flutter packages pub run build_runner watch --delete-conflicting-outputs 57 | 58 | # For information on the generic Dart part of this file, see the 59 | # following page: https://dart.dev/tools/pub/pubspec 60 | 61 | # The following section is specific to Flutter. 62 | flutter: 63 | 64 | # The following line ensures that the Material Icons font is 65 | # included with your application, so that you can use the icons in 66 | # the material Icons class. 67 | uses-material-design: true 68 | 69 | # To add assets to your application, add an assets section, like this: 70 | # assets: 71 | # - images/a_dot_burr.jpeg 72 | # - images/a_dot_ham.jpeg 73 | 74 | # An image asset can refer to one or more resolution-specific "variants", see 75 | # https://flutter.dev/assets-and-images/#resolution-aware. 76 | 77 | # For details regarding adding assets from package dependencies, see 78 | # https://flutter.dev/assets-and-images/#from-packages 79 | 80 | # To add custom fonts to your application, add a fonts section here, 81 | # in this "flutter" section. Each entry in this list should have a 82 | # "family" key with the font family name, and a "fonts" key with a 83 | # list giving the asset and other descriptors for the font. For 84 | # example: 85 | # fonts: 86 | # - family: Schyler 87 | # fonts: 88 | # - asset: fonts/Schyler-Regular.ttf 89 | # - asset: fonts/Schyler-Italic.ttf 90 | # style: italic 91 | # - family: Trajan Pro 92 | # fonts: 93 | # - asset: fonts/TrajanPro.ttf 94 | # - asset: fonts/TrajanPro_Bold.ttf 95 | # weight: 700 96 | # 97 | # For details regarding fonts from package dependencies, 98 | # see https://flutter.dev/custom-fonts/#from-packages 99 | 100 | assets: 101 | - lib/assets/avatar.png 102 | - lib/assets/circle.png 103 | - lib/assets/limit.png 104 | - lib/assets/reminder.png 105 | - lib/assets/task.png 106 | 107 | fonts: 108 | - family: iconfont 109 | fonts: 110 | - asset: lib/fonts/font.ttf -------------------------------------------------------------------------------- /lib/stores/reminder.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/api.dart'; 2 | import 'package:bill/bean/reminder.dart'; 3 | import 'package:bill/http/http-util.dart'; 4 | import 'package:bill/stores/base.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | import 'package:mobx/mobx.dart'; 7 | 8 | part 'reminder.g.dart'; 9 | 10 | class ReminderStore = _ReminderStore with _$ReminderStore; 11 | 12 | abstract class _ReminderStore extends BaseStore with Store { 13 | @observable 14 | List reminder = new List(); 15 | 16 | @observable 17 | ReminderItem current; 18 | 19 | @action 20 | Future createReminder(Map reminder) async { 21 | try { 22 | switchLoading(true); 23 | 24 | Map resp = 25 | await HttpUtil.request(Api.createReminder, reminder, HttpUtil.POST); 26 | HttpResponse data = new HttpResponse.formJson(resp); 27 | 28 | switchLoading(false); 29 | 30 | BotToast.showText(text: data.message); 31 | 32 | return data.success; 33 | } catch (e) { 34 | switchLoading(false); 35 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 36 | return false; 37 | } 38 | } 39 | 40 | Future updateReminder(Map reminder) async { 41 | try { 42 | switchLoading(true); 43 | Map resp = 44 | await HttpUtil.request(Api.updateReminder, reminder, HttpUtil.PUT); 45 | 46 | HttpResponse data = new HttpResponse.formJson(resp); 47 | 48 | switchLoading(false); 49 | 50 | BotToast.showText(text: data.message); 51 | 52 | return data.success; 53 | } catch (e) { 54 | switchLoading(false); 55 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 56 | return false; 57 | } 58 | } 59 | 60 | @action 61 | Future queryReminder([bool toast = false]) async { 62 | try { 63 | switchLoading(true); 64 | 65 | Map resp = 66 | await HttpUtil.request(Api.queryReminder, {}, HttpUtil.GET); 67 | 68 | HttpResponse data = new HttpResponse.formJson(resp); 69 | 70 | if (data.success) { 71 | reminder = new List(); 72 | data.data 73 | .toList() 74 | .forEach((json) => {reminder.add(new ReminderItem.fromJson(json))}); 75 | } 76 | 77 | switchLoading(false); 78 | 79 | if (toast) { 80 | BotToast.showText(text: data.message); 81 | } 82 | return data.success; 83 | } catch (e) { 84 | switchLoading(false); 85 | if (toast) { 86 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 87 | } 88 | return false; 89 | } 90 | } 91 | 92 | @action 93 | Future deleteReminder(String id) async { 94 | try { 95 | switchLoading(true); 96 | 97 | Map resp = 98 | await HttpUtil.request(Api.deleteReminder, { 99 | 'id': id 100 | }, HttpUtil.DELETE); 101 | 102 | HttpResponse data = new HttpResponse.formJson(resp); 103 | 104 | switchLoading(false); 105 | 106 | BotToast.showText(text: data.message); 107 | return data.success; 108 | } catch (e) { 109 | switchLoading(false); 110 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 111 | return false; 112 | } 113 | } 114 | 115 | @action 116 | Future getDetail(String id) async { 117 | try { 118 | switchLoading(true); 119 | 120 | Map resp = 121 | await HttpUtil.request(Api.reminderDetail, {'id': id}, HttpUtil.GET); 122 | 123 | HttpResponse data = new HttpResponse.formJson(resp); 124 | 125 | switchLoading(false); 126 | 127 | if (data.success) { 128 | current = new ReminderItem.fromJson(data.data); 129 | } else { 130 | BotToast.showText(text: data.message); 131 | } 132 | 133 | return data.success; 134 | } catch (e) { 135 | current = null; 136 | switchLoading(false); 137 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 138 | return false; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/bean/statistics.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'statistics.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | CompareLastMonth _$CompareLastMonthFromJson(Map json) { 10 | return CompareLastMonth( 11 | json['limitSetted'] as bool, 12 | json['limit'] as int, 13 | (json['percent'] as num)?.toDouble(), 14 | (json['totalPay'] as num)?.toDouble(), 15 | (json['totalIncome'] as num)?.toDouble(), 16 | (json['totalPayCompare'] as num)?.toDouble(), 17 | (json['totalIncomeCompare'] as num)?.toDouble(), 18 | ); 19 | } 20 | 21 | Map _$CompareLastMonthToJson(CompareLastMonth instance) => 22 | { 23 | 'limitSetted': instance.limitSetted, 24 | 'limit': instance.limit, 25 | 'percent': instance.percent, 26 | 'totalPay': instance.totalPay, 27 | 'totalIncome': instance.totalIncome, 28 | 'totalPayCompare': instance.totalPayCompare, 29 | 'totalIncomeCompare': instance.totalIncomeCompare, 30 | }; 31 | 32 | YearlyAmount _$YearlyAmountFromJson(Map json) { 33 | return YearlyAmount( 34 | (json['totalPay'] as num)?.toDouble(), 35 | (json['totalIncome'] as num)?.toDouble(), 36 | (json['totalRemain'] as num)?.toDouble(), 37 | ); 38 | } 39 | 40 | Map _$YearlyAmountToJson(YearlyAmount instance) => 41 | { 42 | 'totalPay': instance.totalPay, 43 | 'totalIncome': instance.totalIncome, 44 | 'totalRemain': instance.totalRemain, 45 | }; 46 | 47 | MonthliAmount _$MonthliAmountFromJson(Map json) { 48 | return MonthliAmount( 49 | json['billMonth'] as String, 50 | (json['totalPay'] as num)?.toDouble(), 51 | (json['totalIncome'] as num)?.toDouble(), 52 | (json['totalRemain'] as num)?.toDouble(), 53 | ); 54 | } 55 | 56 | Map _$MonthliAmountToJson(MonthliAmount instance) => 57 | { 58 | 'billMonth': instance.billMonth, 59 | 'totalPay': instance.totalPay, 60 | 'totalIncome': instance.totalIncome, 61 | 'totalRemain': instance.totalRemain, 62 | }; 63 | 64 | YearlyAnalyze _$YearlyAnalyzeFromJson(Map json) { 65 | return YearlyAnalyze( 66 | json['yearlyAmounts'] == null 67 | ? null 68 | : YearlyAmount.fromJson(json['yearlyAmounts'] as Map), 69 | (json['monthlyAmounts'] as List) 70 | ?.map((e) => e == null 71 | ? null 72 | : MonthliAmount.fromJson(e as Map)) 73 | ?.toList(), 74 | ); 75 | } 76 | 77 | Map _$YearlyAnalyzeToJson(YearlyAnalyze instance) => 78 | { 79 | 'yearlyAmounts': instance.yearlyAmounts, 80 | 'monthlyAmounts': instance.monthlyAmounts, 81 | }; 82 | 83 | BillAmountsRow _$BillAmountsRowFromJson(Map json) { 84 | return BillAmountsRow( 85 | (json['totalAmount'] as num)?.toDouble(), 86 | json['category'] as String, 87 | ); 88 | } 89 | 90 | Map _$BillAmountsRowToJson(BillAmountsRow instance) => 91 | { 92 | 'totalAmount': instance.totalAmount, 93 | 'category': instance.category, 94 | }; 95 | 96 | DayAmountsRow _$DayAmountsRowFromJson(Map json) { 97 | return DayAmountsRow( 98 | (json['totalAmount'] as num)?.toDouble(), 99 | json['billDate'] as String, 100 | ); 101 | } 102 | 103 | Map _$DayAmountsRowToJson(DayAmountsRow instance) => 104 | { 105 | 'totalAmount': instance.totalAmount, 106 | 'billDate': instance.billDate, 107 | }; 108 | 109 | MonthAnalysis _$MonthAnalysisFromJson(Map json) { 110 | return MonthAnalysis( 111 | (json['monthDayAmount'] as List) 112 | ?.map((e) => e == null 113 | ? null 114 | : DayAmountsRow.fromJson(e as Map)) 115 | ?.toList(), 116 | ); 117 | } 118 | 119 | Map _$MonthAnalysisToJson(MonthAnalysis instance) => 120 | { 121 | 'monthDayAmount': instance.monthDayAmount, 122 | }; 123 | -------------------------------------------------------------------------------- /lib/methods-icons.dart: -------------------------------------------------------------------------------- 1 | // Iconfont 分类以及icon 2 | import 'package:bill/iconfont.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class IconItem { 6 | String type; 7 | IconData icon; 8 | String desc; 9 | 10 | IconItem({this.icon, this.type, this.desc}); 11 | } 12 | 13 | List _paymentIcons = [ 14 | IconItem(icon: IconFont.iconSesameCredit, type: 'p-1', desc: '花呗'), 15 | IconItem(icon: IconFont.iconCreditCard, type: 'p-2', desc: '信用卡'), 16 | IconItem(icon: IconFont.iconGiftMoney, type: 'p-3', desc: '礼金'), 17 | IconItem(icon: IconFont.iconGift, type: 'p-4', desc: '礼物'), 18 | IconItem(icon: IconFont.iconParents, type: 'p-5', desc: '孝敬长辈'), 19 | IconItem(icon: IconFont.iconKids, type: 'p-6', desc: '小孩'), 20 | IconItem(icon: IconFont.iconRepair, type: 'p-7', desc: '维修费'), 21 | IconItem(icon: IconFont.iconHome, type: 'p-8', desc: '房贷'), 22 | IconItem(icon: IconFont.iconOffice, type: 'p-9', desc: '公务支出'), 23 | IconItem(icon: IconFont.iconCar, type: 'p-10', desc: '汽车'), 24 | IconItem(icon: IconFont.iconLotteryTicket, type: 'p-11', desc: '彩票'), 25 | IconItem(icon: IconFont.iconPets, type: 'p-13', desc: '宠物'), 26 | IconItem(icon: IconFont.iconBook, type: 'p-14', desc: '书本费'), 27 | IconItem(icon: IconFont.iconMedical, type: 'p-15', desc: '医疗费'), 28 | IconItem(icon: IconFont.iconStudy, type: 'p-16', desc: '学习'), 29 | IconItem(icon: IconFont.iconDigital, type: 'p-17', desc: '数码'), 30 | IconItem(icon: IconFont.iconTrial, type: 'p-18', desc: '旅行'), 31 | IconItem(icon: IconFont.iconSupports, type: 'p-20', desc: '运动'), 32 | IconItem(icon: IconFont.iconBeauty, type: 'p-21', desc: '美容'), 33 | IconItem(icon: IconFont.iconSnack, type: 'p-22', desc: '零食'), 34 | IconItem(icon: IconFont.icoMobile, type: 'p-23', desc: '通讯'), 35 | IconItem(icon: IconFont.iconSofa, type: 'p-24', desc: '家具'), 36 | IconItem(icon: IconFont.iconCommunity, type: 'p-25', desc: '社交'), 37 | IconItem(icon: IconFont.iconEntertainment, type: 'p-26', desc: '娱乐'), 38 | IconItem(icon: IconFont.iconTraffic, type: 'p-27', desc: '交通'), 39 | IconItem(icon: IconFont.iconCloth, type: 'p-28', desc: '服饰'), 40 | IconItem(icon: IconFont.iconShopping, type: 'p-29', desc: '购物'), 41 | IconItem(icon: IconFont.iconRice, type: 'p-30', desc: '餐饮'), 42 | IconItem(icon: IconFont.iconCommon, type: 'p-31', desc: '其他') 43 | ]; 44 | 45 | Map _paymentIconMaps = { for (var v in _paymentIcons) v.type: v }; 46 | 47 | List _incomeIcons = [ 48 | IconItem(icon: IconFont.iconSalaryIncome, type: 'i-1', desc: '工资'), 49 | IconItem(icon: IconFont.iconPluralismIncome, type: 'i-2', desc: '兼职'), 50 | IconItem(icon: IconFont.iconFinancialIncome, type: 'i-3', desc: '理财'), 51 | IconItem(icon: IconFont.iconRedPackageIncome, type: 'i-4', desc: '红包'), 52 | IconItem(icon: IconFont.iconRetirementIncome, type: 'i-5', desc: '退款'), 53 | IconItem(icon: IconFont.iconReimburseIncome, type: 'i-6', desc: '报销'), 54 | IconItem(icon: IconFont.iconMinutesIncome, type: 'i-7', desc: '分红'), 55 | IconItem(icon: IconFont.iconOtherIncome, type: 'i-8', desc: '其他') 56 | ]; 57 | 58 | Map _incomeIconMaps = { for (var v in _incomeIcons) v.type: v }; 59 | 60 | List _circleTypes = [ 61 | IconItem(icon: IconFont.iconDormRoom, type: 'g-1', desc: '寝室'), 62 | IconItem(icon: IconFont.iconOffice, type: 'g-2', desc: '办公室'), 63 | IconItem(icon: IconFont.iconRenting, type: 'g-3', desc: '合租'), 64 | IconItem(icon: IconFont.iconEntertainment, type: 'g-4', desc: '一起玩'), 65 | IconItem(icon: IconFont.iconTrial, type: 'g-5', desc: '旅行'), 66 | IconItem(icon: IconFont.iconFamily, type: 'g-6', desc: '家庭'), 67 | IconItem(icon: IconFont.iconBusiness, type: 'g-7', desc: '生意场'), 68 | IconItem(icon: IconFont.iconClass, type: 'g-8', desc: '班集体'), 69 | IconItem(icon: IconFont.iconSkirt, type: 'g-9', desc: '姐妹淘') 70 | ]; 71 | 72 | Map _circleTypeMaps = { for (var v in _circleTypes) v.type: v }; 73 | 74 | int _paymentIconsLength = _paymentIcons.length; 75 | int _incomeIconsLength = _incomeIcons.length; 76 | int _circleTypesLength = _circleTypes.length; 77 | 78 | class MethodsIcons { 79 | static List paymentIcons = _paymentIcons; 80 | 81 | static Map paymentIconMaps = _paymentIconMaps; 82 | 83 | static List incomeIcons = _incomeIcons; 84 | 85 | static Map incomeIconMaps = _incomeIconMaps; 86 | 87 | static List circleTypes = _circleTypes; 88 | 89 | static Map circleTypeMaps = _circleTypeMaps; 90 | 91 | static int paymentLength = _paymentIconsLength; 92 | 93 | static int incomeLength = _incomeIconsLength; 94 | 95 | static int circleTypesLength = _circleTypesLength; 96 | 97 | static int paymentRowLengthMax = (_paymentIconsLength ~/ 2).ceil(); 98 | 99 | static int incomeRowLengthMax = (_incomeIconsLength ~/ 2).ceil(); 100 | } 101 | -------------------------------------------------------------------------------- /lib/iconfont.dart: -------------------------------------------------------------------------------- 1 | // ICONFONT字体图标 2 | import 'package:flutter/material.dart'; 3 | 4 | class IconFont { 5 | static IconData iconSesameCredit = IconData(0xe60e, fontFamily: 'iconfont'); 6 | 7 | static IconData iconAsset = IconData(0xe668, fontFamily: 'iconfont'); 8 | 9 | static IconData iconEarth = IconData(0xe666, fontFamily: 'iconfont'); 10 | 11 | static IconData iconAlipay = IconData(0xe664, fontFamily: 'iconfont'); 12 | 13 | static IconData iconAdd = IconData(0xe663, fontFamily: 'iconfont'); 14 | 15 | static IconData iconOtherIncome = IconData(0xe661, fontFamily: 'iconfont'); 16 | 17 | static IconData iconFinancialIncome = 18 | IconData(0xe660, fontFamily: 'iconfont'); 19 | 20 | static IconData iconPluralismIncome = 21 | IconData(0xe65f, fontFamily: 'iconfont'); 22 | 23 | static IconData iconSalaryIncome = IconData(0xe65e, fontFamily: 'iconfont'); 24 | 25 | static IconData iconRedPackageIncome = IconData(0xe667, fontFamily: 'iconfont'); 26 | 27 | static IconData iconMinutesIncome = IconData(0xe60c, fontFamily: 'iconfont'); 28 | 29 | static IconData iconRetirementIncome = IconData(0xe62c, fontFamily: 'iconfont'); 30 | 31 | static IconData iconReimburseIncome = IconData(0xe653, fontFamily: 'iconfont'); 32 | 33 | static IconData iconCreditCard = IconData(0xe683, fontFamily: 'iconfont'); 34 | 35 | static IconData iconGiftMoney = IconData(0xe659, fontFamily: 'iconfont'); 36 | 37 | static IconData iconGift = IconData(0xe658, fontFamily: 'iconfont'); 38 | 39 | static IconData iconParents = IconData(0xe657, fontFamily: 'iconfont'); 40 | 41 | static IconData iconKids = IconData(0xe656, fontFamily: 'iconfont'); 42 | 43 | static IconData iconRepair = IconData(0xe655, fontFamily: 'iconfont'); 44 | 45 | static IconData iconHome = IconData(0xe654, fontFamily: 'iconfont'); 46 | 47 | static IconData iconOffice = IconData(0xe669, fontFamily: 'iconfont'); 48 | 49 | static IconData iconCar = IconData(0xe652, fontFamily: 'iconfont'); 50 | 51 | static IconData iconLotteryTicket = IconData(0xe651, fontFamily: 'iconfont'); 52 | 53 | static IconData iconPets = IconData(0xe650, fontFamily: 'iconfont'); 54 | 55 | static IconData iconBook = IconData(0xe64f, fontFamily: 'iconfont'); 56 | 57 | static IconData iconMedical = IconData(0xe64e, fontFamily: 'iconfont'); 58 | 59 | static IconData iconStudy = IconData(0xe64d, fontFamily: 'iconfont'); 60 | 61 | static IconData iconDigital = IconData(0xe64c, fontFamily: 'iconfont'); 62 | 63 | static IconData iconTrial = IconData(0xe64b, fontFamily: 'iconfont'); 64 | 65 | static IconData iconSupports = IconData(0xe64a, fontFamily: 'iconfont'); 66 | 67 | static IconData iconBeauty = IconData(0xe649, fontFamily: 'iconfont'); 68 | 69 | static IconData iconSnack = IconData(0xe648, fontFamily: 'iconfont'); 70 | 71 | static IconData icoMobile = IconData(0xe647, fontFamily: 'iconfont'); 72 | 73 | static IconData iconSofa = IconData(0xe645, fontFamily: 'iconfont'); 74 | 75 | static IconData iconCommunity = IconData(0xe644, fontFamily: 'iconfont'); 76 | 77 | static IconData iconEntertainment = IconData(0xe643, fontFamily: 'iconfont'); 78 | 79 | static IconData iconTraffic = IconData(0xe642, fontFamily: 'iconfont'); 80 | 81 | static IconData iconCloth = IconData(0xe641, fontFamily: 'iconfont'); 82 | 83 | static IconData iconShopping = IconData(0xe640, fontFamily: 'iconfont'); 84 | 85 | static IconData iconRice = IconData(0xe63f, fontFamily: 'iconfont'); 86 | 87 | static IconData iconCommon = IconData(0xe63e, fontFamily: 'iconfont'); 88 | 89 | static IconData iconIndex = IconData(0xe627, fontFamily: 'iconfont'); 90 | 91 | static IconData iconAnalysis = IconData(0xe6db, fontFamily: 'iconfont'); 92 | 93 | static IconData iconWealth = IconData(0xe65c, fontFamily: 'iconfont'); 94 | 95 | static IconData iconMine = IconData(0xe646, fontFamily: 'iconfont'); 96 | 97 | static IconData iconEdit = IconData(0xe60b, fontFamily: 'iconfont'); 98 | 99 | static IconData iconDown = IconData(0xe6eb, fontFamily: 'iconfont'); 100 | 101 | static IconData iconRight = IconData(0xe600, fontFamily: 'iconfont'); 102 | 103 | static IconData iconUp = IconData(0xe6ee, fontFamily: 'iconfont'); 104 | 105 | static IconData iconCircle = IconData(0xe626, fontFamily: 'iconfont'); 106 | 107 | static IconData iconTask = IconData(0xe750, fontFamily: 'iconfont'); 108 | 109 | static IconData iconReminder = IconData(0xe61b, fontFamily: 'iconfont'); 110 | 111 | static IconData iconSettings = IconData(0xe625, fontFamily: 'iconfont'); 112 | 113 | static IconData iconShare = IconData(0xe68d, fontFamily: 'iconfont'); 114 | 115 | static IconData iconRenting = IconData(0xe77a, fontFamily: 'iconfont'); 116 | 117 | static IconData iconClass = IconData(0xe69a, fontFamily: 'iconfont'); 118 | 119 | static IconData iconDormRoom = IconData(0xe68a, fontFamily: 'iconfont'); 120 | 121 | static IconData iconFamily = IconData(0xe620, fontFamily: 'iconfont'); 122 | 123 | static IconData iconBusiness = IconData(0xe718, fontFamily: 'iconfont'); 124 | 125 | static IconData iconSkirt = IconData(0xe66a, fontFamily: 'iconfont'); 126 | 127 | static IconData iconEmpty = IconData(0xe65a, fontFamily: 'iconfont'); 128 | } 129 | -------------------------------------------------------------------------------- /lib/pages/limit/limit-set.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/colors.dart'; 3 | import 'package:bill/router.dart'; 4 | import 'package:bill/stores/limit.dart'; 5 | import 'package:bill/stores/stores.dart'; 6 | import 'package:bot_toast/bot_toast.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | 11 | class LimitSetPage extends StatefulWidget { 12 | @override 13 | State createState() => LimitSetState(); 14 | } 15 | 16 | class LimitSetState extends State { 17 | final TextEditingController _amountController = TextEditingController(); 18 | 19 | final FocusNode _limitFocus = FocusNode(); 20 | 21 | final LimitStore limitStore = AppStores.limitStore; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _queryLimit(); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | _amountController.dispose(); 32 | _limitFocus.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | Future _queryLimit() async { 37 | bool querySuccess = await limitStore.queryLimit(); 38 | if (querySuccess) { 39 | _amountController.text = '${limitStore.limit}'; 40 | } 41 | } 42 | 43 | Future _saveLimit() async { 44 | _limitFocus.unfocus(); 45 | if (_amountController.text.length == 0) { 46 | BotToast.showText(text: '预算金额不能为空'); 47 | return; 48 | } 49 | 50 | final data = await limitStore.setLimit(int.parse(_amountController.text)); 51 | BotToast.showText(text: data.message); 52 | if (data.success) { 53 | AppRouter.back(context); 54 | } 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return Scaffold( 60 | appBar: AppBar( 61 | title: Text('设置月预算', 62 | style: TextStyle( 63 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 64 | body: Container( 65 | margin: EdgeInsets.only( 66 | top: Adaptor.px(20.0), 67 | left: Adaptor.px(10.0), 68 | right: Adaptor.px(10.0)), 69 | padding: EdgeInsets.only( 70 | left: Adaptor.px(10.0), right: Adaptor.px(10.0)), 71 | width: Adaptor.px(1040.0), 72 | child: Wrap( 73 | children: [ 74 | Container( 75 | width: Adaptor.px(1040.0), 76 | height: Adaptor.px(80.0), 77 | decoration: BoxDecoration( 78 | color: AppColors.appWhite, 79 | borderRadius: 80 | BorderRadius.all(Radius.circular(Adaptor.px(10.0))), 81 | border: Border.all( 82 | width: Adaptor.onePx(), color: AppColors.appBorder), 83 | boxShadow: [ 84 | BoxShadow( 85 | color: AppColors.appBlackShadow, 86 | blurRadius: 5.0, 87 | offset: Offset(0, 1.0)) 88 | ]), 89 | child: TextField( 90 | decoration: InputDecoration( 91 | hintText: '请输入金额', 92 | fillColor: Colors.transparent, 93 | counterText: '', 94 | counterStyle: TextStyle(fontSize: 0), 95 | filled: true, 96 | border: InputBorder.none), 97 | maxLength: 8, 98 | inputFormatters: [WhitelistingTextInputFormatter.digitsOnly], 99 | keyboardType: 100 | TextInputType.number, 101 | style: TextStyle( 102 | fontSize: Adaptor.px(28.0), 103 | color: AppColors.appTextDark), 104 | focusNode: _limitFocus, 105 | cursorWidth: 1.0, 106 | cursorColor: AppColors.appTextDark, 107 | textAlign: TextAlign.center, 108 | controller: _amountController)), 109 | GestureDetector( 110 | onTap: _saveLimit, 111 | child: Container( 112 | width: Adaptor.px(1040.0), 113 | height: Adaptor.px(80.0), 114 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 115 | decoration: BoxDecoration( 116 | color: AppColors.appYellow, 117 | borderRadius: BorderRadius.all( 118 | Radius.circular(Adaptor.px(10.0)))), 119 | child: Center( 120 | child: Text('保存', 121 | style: TextStyle( 122 | fontSize: Adaptor.px(32.0), 123 | fontWeight: FontWeight.normal, 124 | color: AppColors.appTextDark)))) 125 | ) 126 | ], 127 | ))); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/stores/user.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bill/api.dart'; 4 | import 'package:bill/bean/user.dart'; 5 | import 'package:bill/http/http-util.dart'; 6 | import 'package:bill/stores/base.dart'; 7 | import 'package:bot_toast/bot_toast.dart'; 8 | import 'package:mobx/mobx.dart'; 9 | import 'package:shared_preferences/shared_preferences.dart'; 10 | 11 | part 'user.g.dart'; 12 | 13 | class UserStore = _UserStore with _$UserStore; 14 | 15 | abstract class _UserStore extends BaseStore with Store { 16 | @observable 17 | bool logined = false; 18 | 19 | @observable 20 | User userInfo; 21 | 22 | @observable 23 | UserBillCount billCount; 24 | 25 | Future getToken() async { 26 | SharedPreferences prefs = await SharedPreferences.getInstance(); 27 | 28 | String token = prefs.getString('userToken'); 29 | return token; 30 | } 31 | 32 | Future setToken(token) async { 33 | SharedPreferences prefs = await SharedPreferences.getInstance(); 34 | prefs.setString('userToken', token); 35 | } 36 | 37 | Future removeToken() async { 38 | SharedPreferences prefs = await SharedPreferences.getInstance(); 39 | prefs.remove('userToken'); 40 | } 41 | 42 | @action 43 | Future ensureLogin() async { 44 | String token = await getToken(); 45 | 46 | if (token == null) { 47 | logined = false; 48 | return false; 49 | } 50 | 51 | Map resp = 52 | await HttpUtil.request(Api.ensureLogined, {}, HttpUtil.GET); 53 | HttpResponse data = new HttpResponse.formJson(resp); 54 | 55 | if (data.success) { 56 | logined = true; 57 | userInfo = new User.fromJson(data.data); 58 | await setToken(userInfo.token); 59 | } else { 60 | removeToken(); 61 | logined = false; 62 | } 63 | 64 | return logined; 65 | } 66 | 67 | @action 68 | Future login(String mobile, String password, [String device]) async { 69 | try { 70 | switchLoading(true); 71 | Map resp = await HttpUtil.request( 72 | Api.login, 73 | {'mobile': mobile, 'password': password, 'device': device}, 74 | HttpUtil.POST); 75 | 76 | switchLoading(false); 77 | 78 | HttpResponse data = new HttpResponse.formJson(resp); 79 | 80 | if (data.success) { 81 | logined = true; 82 | userInfo = new User.fromJson(data.data); 83 | await setToken(userInfo.token); 84 | } else { 85 | logined = false; 86 | BotToast.showText(text: data.message); 87 | } 88 | 89 | return logined; 90 | } catch (e) { 91 | switchLoading(false); 92 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 93 | return false; 94 | } 95 | } 96 | 97 | @action 98 | Future validateCode(String mobile, String code) async { 99 | try { 100 | Map resp = await HttpUtil.request( 101 | Api.validateVCode, {'mobile': mobile, 'code': code}, HttpUtil.GET); 102 | 103 | HttpResponse data = new HttpResponse.formJson(resp); 104 | 105 | bool validateSuccess = data.success; 106 | 107 | if (!validateSuccess) { 108 | BotToast.showText(text: data.message); 109 | } 110 | 111 | return validateSuccess; 112 | } catch (e) { 113 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 114 | return false; 115 | } 116 | } 117 | 118 | @action 119 | Future registerLogin(String mobile, String password, String rid, 120 | [String device]) async { 121 | try { 122 | switchLoading(true); 123 | Map resp = await HttpUtil.request( 124 | Api.registerLogin, 125 | { 126 | 'mobile': mobile, 127 | 'password': password, 128 | 'registrationId': rid, 129 | 'device': device 130 | }, 131 | HttpUtil.POST); 132 | switchLoading(false); 133 | 134 | HttpResponse data = new HttpResponse.formJson(resp); 135 | 136 | if (data.success) { 137 | logined = true; 138 | userInfo = new User.fromJson(data.data); 139 | await setToken(data.data['token']); 140 | } else { 141 | logined = false; 142 | BotToast.showText(text: data.message); 143 | } 144 | return logined; 145 | } catch (e) { 146 | switchLoading(false); 147 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 148 | return false; 149 | } 150 | } 151 | 152 | @action 153 | Future forgot(String mobile, String password) async { 154 | try { 155 | switchLoading(true); 156 | Map resp = await HttpUtil.request( 157 | Api.forgot, {'mobile': mobile, 'password': password}, HttpUtil.PUT); 158 | switchLoading(false); 159 | 160 | HttpResponse data = new HttpResponse.formJson(resp); 161 | 162 | if (!data.success) { 163 | BotToast.showText(text: data.message); 164 | } 165 | 166 | return data.success; 167 | } catch (e) { 168 | switchLoading(false); 169 | BotToast.showText(text: HttpUtil.UNKNOWN_ERROR); 170 | return false; 171 | } 172 | } 173 | 174 | @action 175 | Future getBillCount() async { 176 | try { 177 | switchLoading(true); 178 | Map resp = await HttpUtil.request(Api.billCount, {}, HttpUtil.GET); 179 | 180 | HttpResponse data = new HttpResponse.formJson(resp); 181 | 182 | if (data.success) { 183 | billCount = new UserBillCount.fromJson(data.data); 184 | } else { 185 | billCount = null; 186 | } 187 | 188 | switchLoading(false); 189 | 190 | return data.success; 191 | } catch (e) { 192 | billCount = null; 193 | switchLoading(false); 194 | 195 | return false; 196 | } 197 | } 198 | 199 | @action 200 | bool logout() { 201 | removeToken(); 202 | logined = false; 203 | Future.delayed(new Duration(seconds: 1)).whenComplete(() { 204 | userInfo = null; 205 | }); 206 | return logined; 207 | } 208 | 209 | } -------------------------------------------------------------------------------- /lib/pages/user/login.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | 3 | import 'package:bill/adaptor.dart'; 4 | import 'package:bill/colors.dart'; 5 | import 'package:bill/router.dart'; 6 | import 'package:bill/stores/stores.dart'; 7 | import 'package:bill/stores/user.dart'; 8 | import 'package:device_info/device_info.dart'; 9 | import 'package:flt_telephony_info/flt_telephony_info.dart'; 10 | import 'package:flutter/cupertino.dart'; 11 | import 'package:flutter/material.dart'; 12 | 13 | //MobpushPlugin.getRegistrationId().then((Map ridMap) { 14 | //print(ridMap); 15 | //String regId = ridMap['res'].toString(); 16 | //print('------>#### registrationId: ' + regId); 17 | //}); 18 | 19 | class LoginPage extends StatefulWidget { 20 | LoginPage({@required this.target}); 21 | 22 | final String target; 23 | 24 | @override 25 | State createState() => LoginState(); 26 | } 27 | 28 | class LoginState extends State { 29 | final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); 30 | 31 | final TextEditingController _mobileController = TextEditingController(); 32 | 33 | final TextEditingController _passwordController = TextEditingController(); 34 | 35 | final UserStore userStore = AppStores.userStore; 36 | 37 | String regId; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _initMobile(); 43 | } 44 | 45 | @override 46 | void dispose() { 47 | _mobileController.dispose(); 48 | _passwordController.dispose(); 49 | super.dispose(); 50 | } 51 | 52 | void _initMobile() async { 53 | try { 54 | if (Platform.isAndroid) { 55 | TelephonyInfo info = await FltTelephonyInfo.info; 56 | } 57 | } catch (e) {} 58 | } 59 | 60 | void _toRegister() { 61 | AppRouter.redirectTo(context, 'register'); 62 | } 63 | 64 | void _toForgot() { 65 | AppRouter.toPage(context, 'forgot', false); 66 | } 67 | 68 | Future _toLogin() async { 69 | String mobile = _mobileController.text; 70 | String password = _passwordController.text; 71 | String model; 72 | 73 | try { 74 | AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; 75 | model = androidInfo.model; 76 | } catch (e) {} 77 | 78 | bool loginSuccess = await userStore.login(mobile, password, model); 79 | 80 | if (loginSuccess) { 81 | if (widget.target != '') { 82 | AppRouter.redirectTo(context, widget.target); 83 | } else { 84 | AppRouter.toHome(context); 85 | } 86 | } 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return Scaffold( 92 | appBar: AppBar( 93 | title: Text('登录', 94 | style: TextStyle( 95 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 96 | body: Container( 97 | margin: EdgeInsets.only( 98 | top: Adaptor.px(20.0), 99 | left: Adaptor.px(20.0), 100 | right: Adaptor.px(20.0)), 101 | padding: EdgeInsets.only( 102 | left: Adaptor.px(10.0), right: Adaptor.px(10.0)), 103 | width: Adaptor.px(1020.0), 104 | child: Wrap( 105 | children: [ 106 | TextField( 107 | controller: _mobileController, 108 | keyboardType: TextInputType.phone, 109 | decoration: InputDecoration( 110 | contentPadding: EdgeInsets.only( 111 | top: Adaptor.px(30.0), 112 | bottom: Adaptor.px(30.0), 113 | left: Adaptor.px(20.0)), 114 | hintText: '请输入手机号', 115 | fillColor: AppColors.appWhite, 116 | filled: true, 117 | enabledBorder: UnderlineInputBorder( 118 | borderSide: BorderSide( 119 | color: AppColors.appBorder, width: Adaptor.onePx()), 120 | ), 121 | focusedBorder: UnderlineInputBorder( 122 | borderSide: BorderSide( 123 | color: AppColors.appBorderDark, 124 | width: Adaptor.onePx()))), 125 | ), 126 | TextField( 127 | controller: _passwordController, 128 | keyboardType: TextInputType.visiblePassword, 129 | obscureText: true, 130 | decoration: InputDecoration( 131 | contentPadding: EdgeInsets.only( 132 | top: Adaptor.px(30.0), 133 | bottom: Adaptor.px(30.0), 134 | left: Adaptor.px(20.0)), 135 | hintText: '请输入登录密码', 136 | fillColor: AppColors.appWhite, 137 | filled: true, 138 | enabledBorder: UnderlineInputBorder( 139 | borderSide: BorderSide( 140 | color: AppColors.appBorder, width: Adaptor.onePx()), 141 | ), 142 | focusedBorder: UnderlineInputBorder( 143 | borderSide: BorderSide( 144 | color: AppColors.appBorderDark, 145 | width: Adaptor.onePx()))), 146 | ), 147 | GestureDetector( 148 | onTap: _toLogin, 149 | child: Container( 150 | width: Adaptor.px(1040.0), 151 | height: Adaptor.px(80.0), 152 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 153 | decoration: BoxDecoration( 154 | color: AppColors.appYellow, 155 | borderRadius: BorderRadius.all( 156 | Radius.circular(Adaptor.px(10.0)))), 157 | child: Center( 158 | child: Text('登录', 159 | style: TextStyle( 160 | fontSize: Adaptor.px(32.0), 161 | fontWeight: FontWeight.normal, 162 | color: AppColors.appTextDark)) 163 | ) 164 | ) 165 | ), 166 | Container( 167 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 168 | child: Row( 169 | crossAxisAlignment: CrossAxisAlignment.center, 170 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 171 | children: [ 172 | GestureDetector(onTap: _toForgot, child: Text('忘记密码')), 173 | GestureDetector(onTap: _toRegister, child: Text('注册')), 174 | ], 175 | )) 176 | ], 177 | ))); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/pages/group/groups.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/bean/group.dart'; 3 | import 'package:bill/colors.dart'; 4 | import 'package:bill/methods-icons.dart'; 5 | import 'package:bill/router.dart'; 6 | import 'package:bill/stores/group.dart'; 7 | import 'package:bill/stores/stores.dart'; 8 | import 'package:bill/widgets/empty.dart'; 9 | import 'package:flutter/cupertino.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_mobx/flutter_mobx.dart'; 12 | 13 | class GroupsPage extends StatefulWidget { 14 | @override 15 | State createState() => GroupsState(); 16 | } 17 | 18 | class GroupsState extends State { 19 | final GroupStore groupStore = AppStores.groupStote; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | groupStore.queryGroups(); 25 | } 26 | 27 | void _createGroup() { 28 | AppRouter.toPage(context, 'create-group'); 29 | } 30 | 31 | void _toDetail(int id) { 32 | AppRouter.toPage(context, 'group-detail?id=${id}'); 33 | } 34 | 35 | void _share() {} 36 | 37 | @override 38 | void deactivate() { 39 | super.deactivate(); 40 | bool current = ModalRoute.of(context).isCurrent; 41 | if (current) { 42 | groupStore.queryGroups(); 43 | } 44 | } 45 | 46 | Widget _buildGroupIcon(GroupItem group) { 47 | if (group.usage == '0') { 48 | return Container( 49 | width: Adaptor.px(100.0), 50 | height: Adaptor.px(100.0), 51 | margin: EdgeInsets.only( 52 | right: Adaptor.px(18.0)), 53 | decoration: BoxDecoration( 54 | color: AppColors.appYellow, 55 | borderRadius: BorderRadius.all( 56 | Radius.circular(Adaptor.px(80.0)))), 57 | child: Center( 58 | child: Text(group.name.substring(0, 1), style: TextStyle( 59 | fontSize: Adaptor.px(50.0), 60 | color: AppColors.appWhite 61 | ))), 62 | ); 63 | } 64 | 65 | IconItem _groupItem = MethodsIcons.circleTypeMaps[group.type]; 66 | return Container( 67 | width: Adaptor.px(100.0), 68 | height: Adaptor.px(100.0), 69 | margin: EdgeInsets.only( 70 | right: Adaptor.px(18.0)), 71 | decoration: BoxDecoration( 72 | color: AppColors.appYellow, 73 | borderRadius: BorderRadius.all( 74 | Radius.circular(Adaptor.px(80.0))) 75 | ), 76 | child: Icon( 77 | _groupItem.icon, 78 | size: Adaptor.px(50.0), 79 | color: AppColors.appWhite, 80 | ) 81 | ); 82 | } 83 | 84 | Widget _buildGroups() { 85 | return Observer( 86 | builder: (_) => Container( 87 | width: Adaptor.screenW(), 88 | child: (groupStore.groups != null && groupStore.groups.length > 0) ? SingleChildScrollView( 89 | child: Container( 90 | margin: EdgeInsets.only( 91 | top: Adaptor.px(10.0), 92 | left: Adaptor.px(10.0), 93 | right: Adaptor.px(10.0), 94 | bottom: Adaptor.px(100) 95 | ), 96 | child: Wrap( 97 | children: List.generate(groupStore.groups.length, (int i) { 98 | GroupItem _group = groupStore.groups[i]; 99 | IconItem groupItem = MethodsIcons.circleTypeMaps[_group.type]; 100 | 101 | return GestureDetector( 102 | onTap: () => _toDetail(_group.id), 103 | child: Container( 104 | width: Adaptor.px(1040.0), 105 | margin: EdgeInsets.only( 106 | top: Adaptor.px(10.0), 107 | left: Adaptor.px(10.0), 108 | right: Adaptor.px(10.0), 109 | bottom: Adaptor.px(10.0)), 110 | padding: EdgeInsets.all(Adaptor.px(16.0)), 111 | decoration: BoxDecoration( 112 | color: AppColors.appWhite, 113 | borderRadius: 114 | const BorderRadius.all(Radius.circular(5.0)), 115 | boxShadow: [ 116 | BoxShadow( 117 | color: AppColors.appBlackShadow, 118 | blurRadius: 5.0, 119 | offset: Offset(0, 1.0)) 120 | ]), 121 | child: Row( 122 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 123 | crossAxisAlignment: CrossAxisAlignment.center, 124 | children: [ 125 | Row( 126 | children: [ 127 | _buildGroupIcon(_group), 128 | Column( 129 | crossAxisAlignment: CrossAxisAlignment.start, 130 | children: [ 131 | Text(_group.usage == '0' ? '个人账单' : groupItem.desc, 132 | style: TextStyle( 133 | fontSize: Adaptor.px(32.0), 134 | color: AppColors.appTextDark)), 135 | Padding( 136 | padding: 137 | EdgeInsets.only(top: Adaptor.px(0)), 138 | child: Text(_group.name, 139 | style: TextStyle( 140 | fontSize: Adaptor.px(28.0), 141 | color: AppColors.appTextNormal)) 142 | ) 143 | ] 144 | ) 145 | ], 146 | ) 147 | ], 148 | )) 149 | ); 150 | }).toList() 151 | ), 152 | ), 153 | ) : Empty() 154 | )); 155 | } 156 | 157 | @override 158 | Widget build(BuildContext context) { 159 | return Scaffold( 160 | appBar: AppBar( 161 | title: Text('记账圈子', 162 | style: TextStyle( 163 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 164 | body: Container( 165 | height: double.infinity, 166 | child: Stack(children: [ 167 | _buildGroups(), 168 | Positioned( 169 | left: 0, 170 | bottom: 0, 171 | right: 0, 172 | child: GestureDetector( 173 | onTap: _createGroup, 174 | child: Container( 175 | height: Adaptor.px(80.0), 176 | color: AppColors.appYellow, 177 | child: Center( 178 | child: Text('创建记账圈子', 179 | style: TextStyle( 180 | fontSize: Adaptor.px(28.0), 181 | color: AppColors.appWhite)) 182 | )) 183 | ) 184 | ) 185 | ]))); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lib/pages/task/tasks.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/bean/task.dart'; 3 | import 'package:bill/colors.dart'; 4 | import 'package:bill/iconfont.dart'; 5 | import 'package:bill/methods-icons.dart'; 6 | import 'package:bill/router.dart'; 7 | import 'package:bill/stores/stores.dart'; 8 | import 'package:bill/stores/task.dart'; 9 | import 'package:bill/widgets/empty.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_mobx/flutter_mobx.dart'; 12 | 13 | class TaskPage extends StatefulWidget { 14 | @override 15 | State createState() => TaskState(); 16 | } 17 | 18 | class TaskState extends State with WidgetsBindingObserver { 19 | final TaskStore taskStore = AppStores.taskStore; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | taskStore.queryTask(); 25 | } 26 | 27 | @override 28 | void deactivate() { 29 | super.deactivate(); 30 | bool current = ModalRoute.of(context).isCurrent; 31 | if (current) { 32 | taskStore.queryTask(); 33 | } 34 | } 35 | 36 | void _toCreateTask() { 37 | AppRouter.toPage(context, 'create-task'); 38 | } 39 | 40 | void _toDetail(int id) { 41 | AppRouter.toPage(context, 'task-detail?id=${id}'); 42 | } 43 | 44 | Widget _buildTasks() { 45 | return Observer( 46 | builder: (_) { 47 | return (taskStore.tasks != null && taskStore.tasks.length > 0) 48 | ? SingleChildScrollView( 49 | child: Container( 50 | margin: EdgeInsets.only( 51 | top: Adaptor.px(10.0), 52 | left: Adaptor.px(10.0), 53 | right: Adaptor.px(10.0)), 54 | child: Wrap( 55 | children: 56 | List.generate(taskStore.tasks.length, (int i) { 57 | TaskItem _task = taskStore.tasks[i]; 58 | IconItem _taskCategory = 59 | MethodsIcons.paymentIconMaps[_task.category]; 60 | 61 | return Container( 62 | width: Adaptor.px(1040.0), 63 | margin: EdgeInsets.all(Adaptor.px(10.0)), 64 | padding: EdgeInsets.all(Adaptor.px(10.0)), 65 | decoration: BoxDecoration( 66 | color: AppColors.appWhite, 67 | borderRadius: 68 | const BorderRadius.all(Radius.circular(5.0)), 69 | boxShadow: [ 70 | BoxShadow( 71 | color: AppColors.appBlackShadow, 72 | blurRadius: 5.0, 73 | offset: Offset(0, 1.0)) 74 | ]), 75 | child: GestureDetector( 76 | onTap: () => _toDetail(_task.id), 77 | child: Wrap( 78 | children: [ 79 | Container( 80 | padding: EdgeInsets.only( 81 | top: Adaptor.px(10.0), 82 | left: Adaptor.px(10.0), 83 | right: Adaptor.px(10.0), 84 | bottom: Adaptor.px(16.0)), 85 | decoration: BoxDecoration( 86 | border: Border( 87 | bottom: BorderSide( 88 | width: Adaptor.onePx(), 89 | color: AppColors.appBorder))), 90 | child: Row( 91 | mainAxisAlignment: 92 | MainAxisAlignment.spaceBetween, 93 | children: [ 94 | Row( 95 | crossAxisAlignment: 96 | CrossAxisAlignment.center, 97 | children: [ 98 | Icon( 99 | IconFont.iconTask, 100 | size: Adaptor.px(28.0), 101 | color: AppColors.appYellow, 102 | ), 103 | Text(_taskCategory.desc, 104 | style: TextStyle( 105 | fontSize: 106 | Adaptor.px(24.0), 107 | color: AppColors 108 | .appTextDark)), 109 | _task.confirm == '1' 110 | ? Text('(记账前跟我确认)', 111 | style: TextStyle( 112 | fontSize: 113 | Adaptor.px( 114 | 20.0), 115 | color: AppColors 116 | .appIncome)) 117 | : SizedBox.shrink() 118 | ], 119 | ), 120 | Text(_task.time, 121 | style: TextStyle( 122 | fontSize: Adaptor.px(26.0), 123 | color: 124 | AppColors.appTextDark)) 125 | ])), 126 | Container( 127 | padding: EdgeInsets.all(Adaptor.px(10.0)), 128 | child: Row( 129 | mainAxisAlignment: 130 | MainAxisAlignment.spaceBetween, 131 | children: [ 132 | Text(_task.remark, 133 | style: TextStyle( 134 | fontSize: Adaptor.px(26.0), 135 | color: 136 | AppColors.appTextNormal)) 137 | ], 138 | )) 139 | ], 140 | ))); 141 | }).toList()))) 142 | : Empty(text: '暂无记账任务, 快去添加一个吧~'); 143 | }, 144 | ); 145 | } 146 | 147 | @override 148 | Widget build(BuildContext context) { 149 | return Scaffold( 150 | appBar: AppBar( 151 | title: Text('记账任务', 152 | style: TextStyle( 153 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 154 | body: Container( 155 | height: double.infinity, 156 | child: Stack(children: [ 157 | Container(child: _buildTasks()), 158 | Positioned( 159 | left: 0, 160 | bottom: 0, 161 | right: 0, 162 | child: Container( 163 | height: Adaptor.px(80.0), 164 | color: AppColors.appYellow, 165 | child: Center( 166 | child: FlatButton( 167 | onPressed: _toCreateTask, 168 | child: Text('添加记账任务', 169 | style: TextStyle( 170 | fontSize: Adaptor.px(28.0), 171 | color: AppColors.appWhite)))))) 172 | ]))); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/pages/reminder/reminders.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/adaptor.dart'; 2 | import 'package:bill/bean/reminder.dart'; 3 | import 'package:bill/colors.dart'; 4 | import 'package:bill/iconfont.dart'; 5 | import 'package:bill/router.dart'; 6 | import 'package:bill/stores/reminder.dart'; 7 | import 'package:bill/stores/stores.dart'; 8 | import 'package:bill/util.dart'; 9 | import 'package:bill/widgets/empty.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_mobx/flutter_mobx.dart'; 12 | 13 | class RemindersPage extends StatefulWidget { 14 | @override 15 | State createState() => RemindersState(); 16 | } 17 | 18 | class RemindersState extends State with WidgetsBindingObserver { 19 | final ReminderStore reminderStore = AppStores.reminderStore; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | reminderStore.queryReminder(); 25 | } 26 | 27 | void _toCreateReminder() { 28 | AppRouter.toPage(context, 'create-reminder'); 29 | } 30 | 31 | void _toDetail(int id) { 32 | AppRouter.toPage(context, 'reminder-detail?id=${id}'); 33 | } 34 | 35 | @override 36 | void deactivate() { 37 | super.deactivate(); 38 | bool current = ModalRoute.of(context).isCurrent; 39 | if (current) { 40 | reminderStore.queryReminder(); 41 | } 42 | } 43 | 44 | Widget _buildReminders() { 45 | return Observer( 46 | builder: (_) => Container( 47 | width: Adaptor.screenW(), 48 | child: (reminderStore.reminder != null && 49 | reminderStore.reminder.length > 0) 50 | ? SingleChildScrollView( 51 | child: Container( 52 | margin: EdgeInsets.only( 53 | top: Adaptor.px(10.0), 54 | left: Adaptor.px(10.0), 55 | right: Adaptor.px(10.0), 56 | bottom: Adaptor.px(120.0)), 57 | child: Wrap( 58 | children: List.generate(reminderStore.reminder.length, 59 | (int i) { 60 | ReminderItem _reminder = reminderStore.reminder[i]; 61 | return Container( 62 | width: Adaptor.px(1040.0), 63 | margin: EdgeInsets.all(Adaptor.px(10.0)), 64 | padding: EdgeInsets.all(Adaptor.px(10.0)), 65 | decoration: BoxDecoration( 66 | color: AppColors.appWhite, 67 | borderRadius: 68 | const BorderRadius.all(Radius.circular(5.0)), 69 | boxShadow: [ 70 | BoxShadow( 71 | color: AppColors.appBlackShadow, 72 | blurRadius: 5.0, 73 | offset: Offset(0, 1.0)) 74 | ]), 75 | child: GestureDetector( 76 | onTap: () => _toDetail(_reminder.id), 77 | child: Wrap( 78 | children: [ 79 | Container( 80 | padding: EdgeInsets.only( 81 | top: Adaptor.px(10.0), 82 | left: Adaptor.px(10.0), 83 | right: Adaptor.px(10.0), 84 | bottom: Adaptor.px(16.0)), 85 | decoration: BoxDecoration( 86 | border: Border( 87 | bottom: BorderSide( 88 | width: Adaptor.onePx(), 89 | color: AppColors.appBorder))), 90 | child: Row( 91 | mainAxisAlignment: 92 | MainAxisAlignment.spaceBetween, 93 | children: [ 94 | Row( 95 | crossAxisAlignment: 96 | CrossAxisAlignment.center, 97 | children: [ 98 | Icon( 99 | IconFont.iconReminder, 100 | size: Adaptor.px(28.0), 101 | color: AppColors.appYellow, 102 | ), 103 | Padding( 104 | padding: EdgeInsets.only( 105 | left: Adaptor.px(10.0)), 106 | child: Text('存钱提醒', 107 | style: TextStyle( 108 | fontSize: 109 | Adaptor.px( 110 | 28.0), 111 | color: AppColors 112 | .appTextDark))), 113 | Text( 114 | Util.isAfter(_reminder.time) 115 | ? '(已提醒)' 116 | : '', 117 | style: TextStyle( 118 | fontSize: 119 | Adaptor.px(20.0), 120 | color: AppColors 121 | .appIncome)) 122 | ], 123 | ), 124 | Text( 125 | '${Util.frenquency2Weekday(_reminder.frequency)} ${_reminder.time}', 126 | style: TextStyle( 127 | fontSize: Adaptor.px(26.0), 128 | color: 129 | AppColors.appTextDark)) 130 | ])), 131 | Container( 132 | padding: EdgeInsets.all(Adaptor.px(10.0)), 133 | child: Text( 134 | _reminder.rule == '0' 135 | ? '金额每天递增' 136 | : '金额每天固定', 137 | style: TextStyle( 138 | fontSize: Adaptor.px(26.0), 139 | color: AppColors.appTextNormal, 140 | height: 1.5))) 141 | ], 142 | ))); 143 | }).toList()), 144 | )) 145 | : Empty(text: '暂无存钱提醒, 快去添加一个吧~'))); 146 | } 147 | 148 | @override 149 | Widget build(BuildContext context) { 150 | return Scaffold( 151 | appBar: AppBar( 152 | title: Text('存钱提醒', 153 | style: TextStyle( 154 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 155 | body: Container( 156 | height: double.infinity, 157 | child: Stack(children: [ 158 | _buildReminders(), 159 | Positioned( 160 | left: 0, 161 | bottom: 0, 162 | right: 0, 163 | child: Container( 164 | height: Adaptor.px(80.0), 165 | color: AppColors.appYellow, 166 | child: Center( 167 | child: FlatButton( 168 | onPressed: _toCreateReminder, 169 | child: Text('添加存钱提醒', 170 | style: TextStyle( 171 | fontSize: Adaptor.px(28.0), 172 | color: AppColors.appWhite)))))) 173 | ]))); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:bill/bottom_navigation_widget.dart'; 2 | import 'package:bill/colors.dart'; 3 | import 'package:bill/pages/user/forgot.dart'; 4 | import 'package:bill/pages/group/create-group.dart'; 5 | import 'package:bill/pages/group/edit-group.dart'; 6 | import 'package:bill/pages/group/group-detail.dart'; 7 | import 'package:bill/pages/group/groups.dart'; 8 | import 'package:bill/pages/limit/limit-set.dart'; 9 | import 'package:bill/pages/user/login.dart'; 10 | import 'package:bill/pages/user/profile.dart'; 11 | import 'package:bill/pages/bill/record.dart'; 12 | import 'package:bill/pages/user/register.dart'; 13 | import 'package:bill/pages/reminder/create-reminder.dart'; 14 | import 'package:bill/pages/reminder/edit-reminder.dart'; 15 | import 'package:bill/pages/reminder/reminder-detail.dart'; 16 | import 'package:bill/pages/reminder/reminders.dart'; 17 | import 'package:bill/pages/task/create-task.dart'; 18 | import 'package:bill/pages/task/edit-task.dart'; 19 | import 'package:bill/pages/task/task-detail.dart'; 20 | import 'package:bill/pages/task/tasks.dart'; 21 | import 'package:bill/pages/bill/month-bill-list.dart'; 22 | import 'package:bill/router.dart'; 23 | import 'package:bill/stores/stores.dart'; 24 | import 'package:bot_toast/bot_toast.dart'; 25 | import 'package:fluro/fluro.dart'; 26 | import 'package:flutter/material.dart'; 27 | import 'package:flutter_localizations/flutter_localizations.dart'; 28 | import 'package:flutter_statusbar_manager/flutter_statusbar_manager.dart'; 29 | // import 'package:moblink/moblink.dart'; 30 | 31 | void main() { 32 | try { 33 | WidgetsFlutterBinding.ensureInitialized(); 34 | } catch (e) {} 35 | 36 | Router router = new Router(); 37 | 38 | router.define('tasks', 39 | handler: Handler( 40 | handlerFunc: (BuildContext context, Map params) => 41 | TaskPage()), 42 | transitionType: TransitionType.native); 43 | router.define('create-task', 44 | handler: Handler( 45 | handlerFunc: (BuildContext context, Map params) => 46 | CreateTaskPage()), 47 | transitionType: TransitionType.native); 48 | router.define('edit-task', 49 | handler: Handler( 50 | handlerFunc: (BuildContext context, Map params) => 51 | EditTaskPage(id: (params['id'] != null && params['id'].length > 0) 52 | ? params['id'][0] 53 | : '')), 54 | transitionType: TransitionType.native); 55 | router.define('task-detail', 56 | handler: Handler( 57 | handlerFunc: (BuildContext context, Map params) => 58 | TaskDetailPage( 59 | id: (params['id'] != null && params['id'].length > 0) 60 | ? params['id'][0] 61 | : '')), 62 | transitionType: TransitionType.native); 63 | router.define('record', 64 | handler: Handler( 65 | handlerFunc: (BuildContext context, Map params) => 66 | RecordPage()), 67 | transitionType: TransitionType.native); 68 | router.define('save-reminder', 69 | handler: Handler( 70 | handlerFunc: (BuildContext context, Map params) => 71 | RemindersPage()), 72 | transitionType: TransitionType.native); 73 | router.define('create-reminder', 74 | handler: Handler( 75 | handlerFunc: (BuildContext context, Map params) => 76 | CreateReminderPage()), 77 | transitionType: TransitionType.native); 78 | router.define('reminder-detail', 79 | handler: Handler( 80 | handlerFunc: (BuildContext context, Map params) => 81 | ReminderDetailPage( 82 | id: (params['id'] != null && params['id'].length > 0) 83 | ? params['id'][0] 84 | : '')), 85 | transitionType: TransitionType.native); 86 | router.define('edit-reminder', 87 | handler: Handler( 88 | handlerFunc: (BuildContext context, Map params) => 89 | EditReminderPage( 90 | id: (params['id'] != null && params['id'].length > 0) 91 | ? params['id'][0] 92 | : '')), 93 | transitionType: TransitionType.native); 94 | router.define('limit-set', 95 | handler: Handler( 96 | handlerFunc: (BuildContext context, Map params) => 97 | LimitSetPage()), 98 | transitionType: TransitionType.native); 99 | router.define('groups', 100 | handler: Handler( 101 | handlerFunc: (BuildContext context, Map params) => 102 | GroupsPage()), 103 | transitionType: TransitionType.native); 104 | router.define('create-group', 105 | handler: Handler( 106 | handlerFunc: (BuildContext context, Map params) => 107 | CreateGroupPage()), 108 | transitionType: TransitionType.native); 109 | router.define('edit-group', 110 | handler: Handler( 111 | handlerFunc: (BuildContext context, Map params) => 112 | EditGroupPage( 113 | id: (params['id'] != null && params['id'].length > 0) 114 | ? params['id'][0] 115 | : '')), 116 | transitionType: TransitionType.native); 117 | router.define('group-detail', 118 | handler: Handler( 119 | handlerFunc: (BuildContext context, Map params) => 120 | GroupDetailPage( 121 | id: (params['id'] != null && params['id'].length > 0) 122 | ? params['id'][0] 123 | : '')), 124 | transitionType: TransitionType.native); 125 | router.define('login', 126 | handler: Handler( 127 | handlerFunc: (BuildContext context, Map params) => 128 | LoginPage( 129 | target: 130 | (params['target'] != null && params['target'].length > 0) 131 | ? params['target'][0] 132 | : '')), 133 | transitionType: TransitionType.native); 134 | router.define('register', 135 | handler: Handler( 136 | handlerFunc: (BuildContext context, Map params) => 137 | RegisterPage( 138 | target: 139 | (params['target'] != null && params['target'].length > 0) 140 | ? params['target'][0] 141 | : '')), 142 | transitionType: TransitionType.native); 143 | router.define('forgot', 144 | handler: Handler( 145 | handlerFunc: (BuildContext context, Map params) => 146 | ForgotPage()), 147 | transitionType: TransitionType.native); 148 | router.define('profile', 149 | handler: Handler( 150 | handlerFunc: (BuildContext context, Map params) => 151 | ProfilePage()), 152 | transitionType: TransitionType.native); 153 | router.define('month-bill-list', 154 | handler: Handler( 155 | handlerFunc: (BuildContext context, Map params) => 156 | MonthBillListPage( 157 | month: (params['month'] != null && params['month'].length > 0) 158 | ? params['month'][0] 159 | : ''))); 160 | 161 | AppRouter.router = router; 162 | 163 | runApp(BillApp()); 164 | } 165 | 166 | class BillApp extends StatelessWidget { 167 | @override 168 | Widget build(BuildContext context) { 169 | AppStores.initStores(); 170 | 171 | FlutterStatusbarManager.setColor(Colors.white); 172 | 173 | return BotToastInit( 174 | child: new MaterialApp( 175 | title: '快记账', 176 | debugShowCheckedModeBanner: false, 177 | theme: ThemeData( 178 | splashColor: Colors.transparent, 179 | highlightColor: Colors.transparent, 180 | buttonTheme: ButtonThemeData( 181 | minWidth: double.infinity, 182 | splashColor: Colors.transparent, 183 | highlightColor: Colors.transparent, 184 | textTheme: ButtonTextTheme.accent), 185 | accentColor: AppColors.appTextNormal, 186 | scaffoldBackgroundColor: AppColors.appWhite, 187 | primaryColor: Colors.white, 188 | appBarTheme: AppBarTheme(elevation: 0 // app标题栏阴影 189 | )), 190 | // home: new SplashScreen( 191 | // seconds: 10, 192 | // navigateAfterSeconds: BottomNavigationWidget(), 193 | // title: new Text( 194 | // 'Welcome In SplashScreen', 195 | // style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 20.0), 196 | // ), 197 | // image: new Image.network('https://flutter.cn/images/catalog-widget-placeholder.png'), 198 | // backgroundColor: Colors.white, 199 | // styleTextUnderTheLoader: new TextStyle(), 200 | // photoSize: 100.0, 201 | // loaderColor: Colors.red, 202 | // ), 203 | localizationsDelegates: [ 204 | GlobalMaterialLocalizations.delegate, 205 | GlobalWidgetsLocalizations.delegate 206 | ], 207 | supportedLocales: [Locale('zh', 'CH'), Locale('en', 'US')], 208 | navigatorObservers: [BotToastNavigatorObserver()], 209 | home: BottomNavigationWidget(), 210 | onGenerateRoute: AppRouter.router.generator)); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/pages/user/forgot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | 3 | import 'package:bill/adaptor.dart'; 4 | import 'package:bill/api.dart'; 5 | import 'package:bill/colors.dart'; 6 | import 'package:bill/router.dart'; 7 | import 'package:bill/stores/stores.dart'; 8 | import 'package:bill/stores/user.dart'; 9 | import 'package:bill/util.dart'; 10 | import 'package:bot_toast/bot_toast.dart'; 11 | import 'package:device_info/device_info.dart'; 12 | import 'package:flt_telephony_info/flt_telephony_info.dart'; 13 | import 'package:flutter/cupertino.dart'; 14 | import 'package:flutter/material.dart'; 15 | 16 | class ForgotPage extends StatefulWidget { 17 | @override 18 | State createState() => ForgotState(); 19 | } 20 | 21 | class ForgotState extends State { 22 | final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); 23 | 24 | final TextEditingController _mobileController = TextEditingController(); 25 | 26 | final TextEditingController _passwordController = TextEditingController(); 27 | 28 | final TextEditingController _passwordConfirmController = 29 | TextEditingController(); 30 | 31 | final TextEditingController _vCodeController = TextEditingController(); 32 | 33 | final FocusNode _mobileFocus = FocusNode(); 34 | 35 | final FocusNode _passwordFocus = FocusNode(); 36 | 37 | final FocusNode _passwordConfirmFocus = FocusNode(); 38 | 39 | final FocusNode _vCodeFocus = FocusNode(); 40 | 41 | final UserStore userStore = AppStores.userStore; 42 | 43 | int step = 1; 44 | 45 | String vCodeUrl = ''; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | _initMobile(); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _mobileController.dispose(); 56 | _passwordController.dispose(); 57 | _passwordConfirmController.dispose(); 58 | _vCodeController.dispose(); 59 | 60 | _mobileFocus.dispose(); 61 | _passwordFocus.dispose(); 62 | _passwordConfirmFocus.dispose(); 63 | _vCodeFocus.dispose(); 64 | 65 | super.dispose(); 66 | } 67 | 68 | void _initCodeUrl() { 69 | String mobile = _mobileController.text; 70 | 71 | if (!Util.validMobile(mobile)) { 72 | BotToast.showText(text: '请输入正确的手机号'); 73 | return; 74 | } 75 | 76 | setState(() { 77 | vCodeUrl = '${Api.base}${Api.getVCode}/${mobile}?t=${Util.randomStr(20)}'; 78 | }); 79 | 80 | _nextStep(); 81 | } 82 | 83 | void _updateCodeUrl() { 84 | setState(() { 85 | vCodeUrl = 86 | '${Api.base}${Api.getVCode}/${_mobileController.text}?t=${Util.randomStr(20)}'; 87 | }); 88 | } 89 | 90 | void _nextStep() { 91 | setState(() { 92 | if (step == 1) { 93 | _mobileFocus.unfocus(); 94 | } else { 95 | _vCodeFocus.unfocus(); 96 | } 97 | 98 | step = step + 1; 99 | }); 100 | } 101 | 102 | void _initMobile() async { 103 | try { 104 | if (Platform.isAndroid) { 105 | TelephonyInfo info = await FltTelephonyInfo.info; 106 | } 107 | } catch (e) {} 108 | } 109 | 110 | Future _resetPassword() async { 111 | _passwordFocus.unfocus(); 112 | _passwordConfirmFocus.unfocus(); 113 | 114 | String mobile = _mobileController.text; 115 | String password = _passwordController.text; 116 | String passwordConfirm = _passwordConfirmController.text; 117 | 118 | if (password != passwordConfirm) { 119 | BotToast.showText(text: '两次密码不一致!'); 120 | return; 121 | } 122 | 123 | bool resetSuccess = await userStore.forgot(mobile, password); 124 | 125 | if (resetSuccess) { 126 | AppRouter.back(context); 127 | } 128 | } 129 | 130 | Future _validateCode() async { 131 | String mobile = _mobileController.text; 132 | String vCode = _vCodeController.text; 133 | 134 | if (vCode.length != 6) { 135 | BotToast.showText(text: '验证码为6位!'); 136 | return; 137 | } 138 | 139 | bool validateSuccess = await userStore.validateCode(mobile, vCode); 140 | 141 | if (validateSuccess) { 142 | _nextStep(); 143 | } 144 | } 145 | 146 | Widget _buildFirstStep() { 147 | return Wrap( 148 | children: [ 149 | TextField( 150 | controller: _mobileController, 151 | keyboardType: TextInputType.phone, 152 | focusNode: _mobileFocus, 153 | decoration: InputDecoration( 154 | contentPadding: EdgeInsets.only( 155 | top: Adaptor.px(30.0), 156 | bottom: Adaptor.px(30.0), 157 | left: Adaptor.px(20.0)), 158 | hintText: '请输入手机号', 159 | fillColor: AppColors.appWhite, 160 | filled: true, 161 | enabledBorder: UnderlineInputBorder( 162 | borderSide: BorderSide( 163 | color: AppColors.appBorder, width: Adaptor.onePx()), 164 | ), 165 | focusedBorder: UnderlineInputBorder( 166 | borderSide: BorderSide( 167 | color: AppColors.appBorderDark, width: Adaptor.onePx()))), 168 | ), 169 | GestureDetector( 170 | onTap: _initCodeUrl, 171 | child: Container( 172 | width: Adaptor.px(1040.0), 173 | height: Adaptor.px(80.0), 174 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 175 | decoration: BoxDecoration( 176 | color: AppColors.appYellow, 177 | borderRadius: 178 | BorderRadius.all(Radius.circular(Adaptor.px(10.0)))), 179 | child: Center( 180 | child: Text('下一步', 181 | style: TextStyle( 182 | fontSize: Adaptor.px(32.0), 183 | fontWeight: FontWeight.normal, 184 | color: AppColors.appTextDark))))) 185 | ], 186 | ); 187 | } 188 | 189 | Widget _buildSecondStep() { 190 | return Wrap( 191 | children: [ 192 | Flex( 193 | direction: Axis.horizontal, 194 | crossAxisAlignment: CrossAxisAlignment.end, 195 | children: [ 196 | Expanded( 197 | child: TextField( 198 | controller: _vCodeController, 199 | focusNode: _vCodeFocus, 200 | autocorrect: false, 201 | decoration: InputDecoration( 202 | contentPadding: EdgeInsets.only( 203 | top: Adaptor.px(30.0), 204 | bottom: Adaptor.px(30.0), 205 | left: Adaptor.px(20.0)), 206 | hintText: '请输入图片验证码', 207 | fillColor: AppColors.appWhite, 208 | filled: true, 209 | enabledBorder: UnderlineInputBorder( 210 | borderSide: BorderSide( 211 | color: AppColors.appBorder, width: Adaptor.onePx()), 212 | ), 213 | focusedBorder: UnderlineInputBorder( 214 | borderSide: BorderSide( 215 | color: AppColors.appBorderDark, 216 | width: Adaptor.onePx()))), 217 | )), 218 | GestureDetector( 219 | onTap: _updateCodeUrl, 220 | child: Container( 221 | child: Image.network( 222 | vCodeUrl, 223 | width: Adaptor.px(240), 224 | height: Adaptor.px(80), 225 | ))), 226 | ], 227 | ), 228 | GestureDetector( 229 | onTap: _validateCode, 230 | child: Container( 231 | width: Adaptor.px(1040.0), 232 | height: Adaptor.px(80.0), 233 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 234 | decoration: BoxDecoration( 235 | color: AppColors.appYellow, 236 | borderRadius: 237 | BorderRadius.all(Radius.circular(Adaptor.px(10.0)))), 238 | child: Center( 239 | child: Text('下一步', 240 | style: TextStyle( 241 | fontSize: Adaptor.px(32.0), 242 | fontWeight: FontWeight.normal, 243 | color: AppColors.appTextDark))))) 244 | ], 245 | ); 246 | } 247 | 248 | Widget _buildThirdStep() { 249 | return Wrap( 250 | children: [ 251 | TextField( 252 | controller: _passwordController, 253 | keyboardType: TextInputType.visiblePassword, 254 | obscureText: true, 255 | focusNode: _passwordFocus, 256 | decoration: InputDecoration( 257 | contentPadding: EdgeInsets.only( 258 | top: Adaptor.px(30.0), 259 | bottom: Adaptor.px(30.0), 260 | left: Adaptor.px(20.0)), 261 | hintText: '设置密���', 262 | fillColor: AppColors.appWhite, 263 | filled: true, 264 | enabledBorder: UnderlineInputBorder( 265 | borderSide: BorderSide( 266 | color: AppColors.appBorder, width: Adaptor.onePx()), 267 | ), 268 | focusedBorder: UnderlineInputBorder( 269 | borderSide: BorderSide( 270 | color: AppColors.appBorderDark, width: Adaptor.onePx()))), 271 | ), 272 | TextField( 273 | controller: _passwordConfirmController, 274 | keyboardType: TextInputType.visiblePassword, 275 | obscureText: true, 276 | focusNode: _passwordConfirmFocus, 277 | decoration: InputDecoration( 278 | contentPadding: EdgeInsets.only( 279 | top: Adaptor.px(30.0), 280 | bottom: Adaptor.px(30.0), 281 | left: Adaptor.px(20.0)), 282 | hintText: '确认密码', 283 | fillColor: AppColors.appWhite, 284 | filled: true, 285 | enabledBorder: UnderlineInputBorder( 286 | borderSide: BorderSide( 287 | color: AppColors.appBorder, width: Adaptor.onePx()), 288 | ), 289 | focusedBorder: UnderlineInputBorder( 290 | borderSide: BorderSide( 291 | color: AppColors.appBorderDark, width: Adaptor.onePx()))), 292 | ), 293 | GestureDetector( 294 | onTap: _resetPassword, 295 | child: Container( 296 | width: Adaptor.px(1040.0), 297 | height: Adaptor.px(80.0), 298 | margin: EdgeInsets.only(top: Adaptor.px(20.0)), 299 | decoration: BoxDecoration( 300 | color: AppColors.appYellow, 301 | borderRadius: 302 | BorderRadius.all(Radius.circular(Adaptor.px(10.0)))), 303 | child: Center( 304 | child: Text('确定', 305 | style: TextStyle( 306 | fontSize: Adaptor.px(32.0), 307 | fontWeight: FontWeight.normal, 308 | color: AppColors.appTextDark))))) 309 | ], 310 | ); 311 | } 312 | 313 | @override 314 | Widget build(BuildContext context) { 315 | return Scaffold( 316 | appBar: AppBar( 317 | title: Text('忘记密码', 318 | style: TextStyle( 319 | fontSize: Adaptor.px(32.0), color: AppColors.appTextDark))), 320 | body: Container( 321 | margin: EdgeInsets.only( 322 | top: Adaptor.px(20.0), 323 | left: Adaptor.px(20.0), 324 | right: Adaptor.px(20.0)), 325 | padding: EdgeInsets.only( 326 | left: Adaptor.px(10.0), right: Adaptor.px(10.0)), 327 | width: Adaptor.px(1020.0), 328 | child: step == 1 329 | ? _buildFirstStep() 330 | : step == 2 ? _buildSecondStep() : _buildThirdStep())); 331 | } 332 | } 333 | --------------------------------------------------------------------------------