diff --git a/assets/images/loading.png b/assets/images/loading.png new file mode 100644 index 0000000..9a9d0d5 Binary files /dev/null and b/assets/images/loading.png differ diff --git a/lib/base/base_state_widget.dart b/lib/base/base_state_widget.dart index 8e3f53c..b968c51 100644 --- a/lib/base/base_state_widget.dart +++ b/lib/base/base_state_widget.dart @@ -24,9 +24,13 @@ class _BaseStateWidgetState extends ConsumerState(widget.model)); - } + WidgetsBinding.instance?.addPostFrameCallback( + (timeStamp) { + if (widget.onReady != null) { + widget.onReady!(ref.read(widget.model)); + } + }, + ); } @override diff --git a/lib/base/base_viewmodel.dart b/lib/base/base_viewmodel.dart index 94c861a..b4ca389 100644 --- a/lib/base/base_viewmodel.dart +++ b/lib/base/base_viewmodel.dart @@ -1,6 +1,10 @@ import 'package:flutter/cupertino.dart'; -class BaseViewModel extends ChangeNotifier { + +class ViewModel extends ChangeNotifier{} + + +class BaseViewModel extends ViewModel { PageState currentState = PageState.START; void loading({bool notify = false}) { diff --git a/lib/base/common_dialog.dart b/lib/base/common_dialog.dart new file mode 100644 index 0000000..12dc515 --- /dev/null +++ b/lib/base/common_dialog.dart @@ -0,0 +1,196 @@ + +import 'dart:async'; + +import 'package:back_button_interceptor/back_button_interceptor.dart'; +import 'package:flutter/material.dart'; + +/// @author newtab on 2021/7/16 +///常用loading,fail,success对话框 +class CommonDialog extends StatelessWidget { + final String? text; + final CommonDialogState? commonDialogState; + + const CommonDialog({Key? key, this.text, this.commonDialogState}) : super(key: key); + + @override + Widget build(BuildContext context) { + Widget result = Material( + color: Colors.transparent, + child: Align( + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration(color: Color.fromRGBO(17, 17, 17, 0.7), borderRadius: BorderRadius.circular(5)), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width / 2, + minWidth: 122, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(top: 15.0), + constraints: BoxConstraints( + minHeight: 40, + ), + child: IconTheme( + data: IconThemeData( + color: Colors.white, + size: 40.0, + ), + child: LoadingIcon()), + ), + SizedBox( + height: 15, + ), + if (text != null) + Padding( + padding: EdgeInsets.only( + left: 15, + right: 15, + bottom: 15, + ), + child: DefaultTextStyle( + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Colors.white, fontSize: 14), + child: Text(text!), + ), + ), + ], + ), + ), + ), + ); + + return result; + } +} + +class LoadingIcon extends StatefulWidget { + final double size; + + LoadingIcon({this.size = 50.0}); + + @override + State createState() => LoadingIconState(); +} + +class LoadingIconState extends State with SingleTickerProviderStateMixin { + AnimationController? _controller; + Animation? _doubleAnimation; + + @override + void initState() { + super.initState(); + _controller = new AnimationController(vsync: this, duration: Duration(milliseconds: 1000))..repeat(); + _doubleAnimation = Tween(begin: 0.0, end: 360.0).animate(_controller!) + ..addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: _doubleAnimation!.value ~/ 30 * 30.0 * 0.0174533, child: Image.asset("assets/images/loading.png", width: widget.size, height: widget.size)); + } +} + +enum CommonDialogState { + LOADING, + SUCCESS, + FAILED, +} + +typedef HideCallback = Future Function(); + +int backButtonIndex = 2; + +HideCallback showCommonDialog( + BuildContext context, { + String? text, + bool backButtonClose = false, + CommonDialogState commonDialogState = CommonDialogState.LOADING, + }) { + Completer result = Completer(); + var backButtonName = 'QL_Toast$backButtonIndex'; + BackButtonInterceptor.add((stopDefaultButtonEvent, _) { + if (backButtonClose) { + result.future.then((hide) { + hide(); + }); + } + return true; + }, zIndex: backButtonIndex, name: backButtonName); + backButtonIndex++; + + var overlay = OverlayEntry( + maintainState: true, + builder: (_) => WillPopScope( + onWillPop: () async { + var hide = await result.future; + hide(); + return false; + }, + child: CommonDialog( + text: text, + ), + ), + ); + result.complete(() { + overlay.remove(); + BackButtonInterceptor.removeByName(backButtonName); + }); + Overlay.of(context)?.insert(overlay); + + return () async { + var hide = await result.future; + hide(); + }; +} + +Future successDialog(BuildContext context, String text) { + HideCallback hideCallback = showCommonDialog( + context, + text: text, + commonDialogState: CommonDialogState.SUCCESS, + backButtonClose: true, + ); + return Future.delayed( + Duration( + milliseconds: 1000, + ), () { + hideCallback(); + }); +} + +Future failDialog(BuildContext context, String text) { + HideCallback hideCallback = showCommonDialog( + context, + text: text, + commonDialogState: CommonDialogState.FAILED, + backButtonClose: true, + ); + return Future.delayed( + Duration( + milliseconds: 1000, + ), () { + hideCallback(); + }); +} + +HideCallback loadingDialog(BuildContext context, {String? text}) { + return showCommonDialog( + context, + text: text, + commonDialogState: CommonDialogState.LOADING, + backButtonClose: true, + ); +} \ No newline at end of file diff --git a/lib/base/routes.dart b/lib/base/routes.dart new file mode 100644 index 0000000..3346d34 --- /dev/null +++ b/lib/base/routes.dart @@ -0,0 +1,16 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:qinglong_app/module/home/home_page.dart'; + +class Routes { + static const String route_HomePage = "/home/homepage"; + + static Route? generateRoute(RouteSettings settings) { + switch (settings.name) { + case route_HomePage: + return CupertinoPageRoute(builder: (context) => const HomePage()); + } + + return null; + } +} diff --git a/lib/base/sp_const.dart b/lib/base/sp_const.dart new file mode 100644 index 0000000..87baf99 --- /dev/null +++ b/lib/base/sp_const.dart @@ -0,0 +1 @@ +String sp_UserINfo = "userinfo"; \ No newline at end of file diff --git a/lib/base/theme.dart b/lib/base/theme.dart index 3847e56..d96bc51 100644 --- a/lib/base/theme.dart +++ b/lib/base/theme.dart @@ -16,5 +16,9 @@ class ThemeViewModel extends ChangeNotifier { } } -ThemeData darkTheme = ThemeData.dark().copyWith(); -ThemeData lightTheme = ThemeData.light().copyWith(); +ThemeData darkTheme = ThemeData.dark().copyWith( + primaryColor: Color(0xffffffff), +); +ThemeData lightTheme = ThemeData.light().copyWith( + primaryColor: Color(0xFF0F77FE), +); diff --git a/lib/base/userinfo_viewmodel.dart b/lib/base/userinfo_viewmodel.dart new file mode 100644 index 0000000..a1ba4ba --- /dev/null +++ b/lib/base/userinfo_viewmodel.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qinglong_app/base/base_viewmodel.dart'; +import 'package:qinglong_app/utils/sp_utils.dart'; + +import 'sp_const.dart'; + +var userInfoProvider = ChangeNotifierProvider((ref) => UserInfoViewModel()); + +class UserInfoViewModel extends ViewModel { + UserInfoBean? _userInfoBean; + + UserInfoViewModel() { + String userInfoJson = SpUtil.getString(sp_UserINfo); + if (userInfoJson.isNotEmpty) { + _userInfoBean = UserInfoBean.fromJson(jsonDecode(userInfoJson)); + } + } + + void updateUserInfoBean(UserInfoBean userInfoBean) { + _userInfoBean = userInfoBean; + SpUtil.putString(sp_UserINfo, jsonEncode(userInfoBean)); + } + + UserInfoBean? get userInfoBean => _userInfoBean; +} + +class UserInfoBean { + String? name; + int? age; + + UserInfoBean({this.name, this.age}); + + UserInfoBean.fromJson(Map json) { + name = json['name']; + age = json['age']; + } + + Map toJson() { + final Map data = new Map(); + data['name'] = this.name; + data['age'] = this.age; + return data; + } +} diff --git a/lib/main.dart b/lib/main.dart index 60448c8..bfcf0c3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qinglong_app/base/theme.dart'; +import 'package:qinglong_app/module/login/login_page.dart'; +import 'base/routes.dart'; +import 'base/userinfo_viewmodel.dart'; import 'module/home/home_page.dart'; void main() { @@ -12,6 +15,7 @@ void main() { ProviderScope( overrides: [ themeProvider, + userInfoProvider, ], child: const MyApp(), ), @@ -27,9 +31,18 @@ class MyApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp( - theme: ref.watch(themeProvider).currentTheme, - home: HomePage(), + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: (){ + FocusScope.of(context).requestFocus(FocusNode()); + }, + child: MaterialApp( + theme: ref.watch(themeProvider).currentTheme, + onGenerateRoute: (setting) { + return Routes.generateRoute(setting); + }, + home: ref.read(userInfoProvider).userInfoBean != null ? const HomePage() : LoginPage(), + ), ); } } diff --git a/lib/module/login/login_page.dart b/lib/module/login/login_page.dart new file mode 100644 index 0000000..c04899b --- /dev/null +++ b/lib/module/login/login_page.dart @@ -0,0 +1,154 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qinglong_app/base/common_dialog.dart'; +import 'package:qinglong_app/base/routes.dart'; +import 'package:qinglong_app/module/login/login_viewmodel.dart'; +import 'package:qinglong_app/utils/utils.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({Key? key}) : super(key: key); + + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController _userNameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + HideCallback? _hideCallback; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Consumer(builder: (context, ref, child) { + var model = ref.watch(loginProvider); + + if (model.isLoading) { + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + _hideCallback ??= loadingDialog(context, text: "登录中..."); + }); + } else { + if (_hideCallback != null) { + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + _hideCallback!(); + }); + } + } + + if (model.loginSuccess) { + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) { + Navigator.of(context).pushNamed(Routes.route_HomePage); + }); + } + return SizedBox( + height: MediaQuery.of(context).size.height, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: MediaQuery.of(context).size.height / 3, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: Row( + children: [ + const SizedBox( + child: Text( + "用户名:", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + width: 60, + ), + const SizedBox( + width: 15, + ), + Expanded( + child: TextField( + onChanged: (_) { + setState(() {}); + }, + controller: _userNameController, + decoration: const InputDecoration( + contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), + hintText: "请输入用户名", + ), + autofocus: false, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 10, + ), + child: Row( + children: [ + const SizedBox( + child: Text( + "密码:", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + width: 60, + ), + const SizedBox( + width: 15, + ), + Expanded( + child: TextField( + onChanged: (_) { + setState(() {}); + }, + controller: _passwordController, + obscureText: true, + decoration: const InputDecoration( + contentPadding: EdgeInsets.fromLTRB(0, 5, 0, 5), + hintText: "请输入密码", + ), + autofocus: false, + ), + ), + ], + ), + ), + const SizedBox( + height: 30, + ), + SizedBox( + width: MediaQuery.of(context).size.width - 30, + child: CupertinoButton( + color: (_userNameController.text.isNotEmpty && _passwordController.text.isNotEmpty) + ? Theme.of(context).primaryColor + : Theme.of(context).primaryColor.withOpacity(0.3), + child: const Text( + "登录", + style: TextStyle( + fontSize: 16, + ), + ), + onPressed: () { + Utils.hideKeyBoard(context); + model.login(_userNameController.text, _passwordController.text); + }), + ), + ], + ), + ), + ); + }), + ); + } +} diff --git a/lib/module/login/login_viewmodel.dart b/lib/module/login/login_viewmodel.dart new file mode 100644 index 0000000..72b98a7 --- /dev/null +++ b/lib/module/login/login_viewmodel.dart @@ -0,0 +1,19 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:qinglong_app/base/base_viewmodel.dart'; + +var loginProvider = ChangeNotifierProvider((ref) => LoginViewModel()); + +class LoginViewModel extends ViewModel { + bool loginSuccess = false; + bool isLoading = false; + + void login(String userName, String password) { + isLoading = true; + notifyListeners(); + Future.delayed(Duration(seconds: 2), () { + isLoading = false; + loginSuccess = true; + notifyListeners(); + }); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..f8e7544 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,7 @@ +import 'package:flutter/cupertino.dart'; + +class Utils { + static void hideKeyBoard(BuildContext context) { + FocusScope.of(context).requestFocus(FocusNode()); + } +} diff --git a/pubspec.lock b/pubspec.lock index ba7941b..bbedd48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.8.2" + back_button_interceptor: + dependency: "direct main" + description: + name: back_button_interceptor + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2cca50a..9e8b6f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter_riverpod: ^1.0.3 shared_preferences: ^2.0.12 synchronized: ^3.0.0 + back_button_interceptor: ^5.0.2 dev_dependencies: flutter_test: @@ -31,9 +32,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/images/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.