From 05e4fd6f667855af31ada019575394de1c463133 Mon Sep 17 00:00:00 2001 From: jyuesong <425698907@qq.com> Date: Wed, 12 Jan 2022 15:45:52 +0800 Subject: [PATCH] add login --- assets/images/loading.png | Bin 0 -> 2977 bytes lib/base/base_state_widget.dart | 10 +- lib/base/base_viewmodel.dart | 6 +- lib/base/common_dialog.dart | 196 ++++++++++++++++++++++++++ lib/base/routes.dart | 16 +++ lib/base/sp_const.dart | 1 + lib/base/theme.dart | 8 +- lib/base/userinfo_viewmodel.dart | 46 ++++++ lib/main.dart | 19 ++- lib/module/login/login_page.dart | 154 ++++++++++++++++++++ lib/module/login/login_viewmodel.dart | 19 +++ lib/utils/utils.dart | 7 + pubspec.lock | 7 + pubspec.yaml | 6 +- 14 files changed, 483 insertions(+), 12 deletions(-) create mode 100644 assets/images/loading.png create mode 100644 lib/base/common_dialog.dart create mode 100644 lib/base/routes.dart create mode 100644 lib/base/sp_const.dart create mode 100644 lib/base/userinfo_viewmodel.dart create mode 100644 lib/module/login/login_page.dart create mode 100644 lib/module/login/login_viewmodel.dart create mode 100644 lib/utils/utils.dart diff --git a/assets/images/loading.png b/assets/images/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9d0d56eff5181cf129922349ce6372647acac3 GIT binary patch literal 2977 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 X8GQjgx 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.