├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── README.md ├── analysis_options.yaml ├── assets ├── fonts │ └── iconfont.ttf └── images │ ├── addnew.png │ ├── avator1.jpg │ ├── avator2.jpg │ ├── avator3.jpg │ ├── avator4.jpg │ ├── avator5.jpg │ ├── cardpackage.png │ ├── collect.png │ ├── contact.png │ ├── emoij.png │ ├── file.jpg │ ├── find.png │ ├── group.png │ ├── icon.png │ ├── label.png │ ├── message.png │ ├── mine.png │ ├── miniprogram.png │ ├── more.png │ ├── next.png │ ├── picture.png │ ├── public.png │ ├── qrcode.png │ ├── scan.png │ ├── search.png │ ├── searchs.png │ ├── setting.png │ ├── shake.png │ ├── video.png │ ├── view.png │ ├── wechat_moment.png │ └── wxpay.png ├── lib ├── common │ ├── apis │ │ ├── group.dart │ │ ├── message.dart │ │ └── user.dart │ ├── biz │ │ ├── custom_class.dart │ │ └── websocket.dart │ ├── entities │ │ ├── group.dart │ │ ├── index.dart │ │ ├── message.dart │ │ └── user.dart │ ├── langs │ │ ├── en_US.dart │ │ ├── translation_service.dart │ │ └── zh_Hans.dart │ ├── middlewares │ │ └── auth.dart │ ├── mock │ │ └── data.dart │ ├── routes │ │ └── routes.dart │ ├── store │ │ └── user.dart │ ├── style │ │ ├── colors.dart │ │ ├── icons.dart │ │ └── menus.dart │ ├── utils │ │ ├── http.dart │ │ ├── index.dart │ │ ├── loading.dart │ │ ├── platform.dart │ │ ├── storage.dart │ │ ├── timex.dart │ │ └── utils.dart │ ├── values │ │ └── storage.dart │ ├── widgets │ │ ├── app_bar.dart │ │ ├── chat_content.dart │ │ ├── network_img.dart │ │ ├── password.dart │ │ ├── search_bar.dart │ │ └── user_avatar.dart │ ├── xerr │ │ ├── codes.dart │ │ ├── index.dart │ │ └── msgs.dart │ └── xresp │ │ └── xresp.dart ├── global.dart ├── main.dart └── pages │ ├── chat │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ ├── discover │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ ├── frame │ ├── login │ │ ├── bindings.dart │ │ ├── controller.dart │ │ ├── index.dart │ │ └── view.dart │ └── notfound │ │ ├── bindings.dart │ │ ├── controller.dart │ │ ├── index.dart │ │ └── view.dart │ ├── friend │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ ├── home │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ ├── message │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ ├── mine │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart │ └── more │ └── add_friend │ ├── bindings.dart │ ├── controller.dart │ ├── index.dart │ └── view.dart ├── pubspec.yaml └── test └── widget_test.dart /.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 | web/ 18 | macos/ 19 | dart_demo/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | windows/ 37 | ios/ 38 | linux/ 39 | web/ 40 | 41 | # Web related 42 | lib/generated_plugin_registrant.dart 43 | 44 | # Symbolication related 45 | app.*.symbols 46 | 47 | # Obfuscation related 48 | app.*.map.json 49 | 50 | # Android Studio will place build artifacts here 51 | /android/app/debug 52 | /android/app/profile 53 | /android/app/release 54 | ### Dart template 55 | # See https://www.dartlang.org/guides/libraries/private-files 56 | 57 | # Files and directories created by pub 58 | .dart_tool/ 59 | .packages 60 | build/ 61 | # If you're building an application, you may want to check-in your pubspec.lock 62 | pubspec.lock 63 | 64 | # Directory created by dartdoc 65 | # If you don't generate documentation locally you can remove this line. 66 | doc/api/ 67 | 68 | # Avoid committing generated Javascript files: 69 | *.dart.js 70 | *.info.json # Produced by the --dump-info flag. 71 | *.js # When generated by dart2js. Don't specify *.js if your 72 | # project includes source files written in JavaScript. 73 | *.js_ 74 | *.js.deps 75 | *.js.map 76 | 77 | .flutter-plugins 78 | .flutter-plugins-dependencies 79 | 80 | -------------------------------------------------------------------------------- /.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: 77d935af4db863f6abd0b9c31c7e6df2a13de57b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "wechat_flutter", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "wechat_flutter (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | }, 18 | { 19 | "name": "wechat_flutter (release mode)", 20 | "request": "launch", 21 | "type": "dart", 22 | "flutterMode": "release" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat_flutter 2 | ![dart](https://img.shields.io/badge/dart-2.16-blue.svg) 3 | ![flutter](https://img.shields.io/badge/flutter-2.10-blue.svg) 4 | ![get](https://img.shields.io/badge/get-4.6-blue.svg) 5 | ![star](https://img.shields.io/github/stars/wslynn/wechat_flutter?style=social) 6 | 7 | 8 | ## 1 项目简介 9 | 仿微信即时通信软件(基于GetX和Flutter框架) 10 | ![](https://img.gejiba.com/images/660ff7cde6d11c49e1ca7c8340a4057a.gif) 11 | 12 | 13 | ## 2 common目录说明 14 | - apis: 访问服务器的接口封装 15 | - entities: 网络请求与响应体结构 16 | - langs: 国际化多语言翻译 17 | - middlewares: 中间件, 如是否登录鉴权 18 | - mock: 模拟的假数据, 渲染到界面上展示效果 19 | - models: 请求与响应的结构体 20 | - routes: app路由配置 21 | - store: 本地数据存储, 调用utils/storage 22 | - utils: 跟业务逻辑无关的工具包 23 | - storage: 根据平台不同, 存储数据到cookie或本机路径等 24 | - http: http请求封装 25 | - loading: 加载页面 26 | 27 | 28 | ## 3 后端接口文档 29 | [ApiPost接口文档](https://console-docs.apipost.cn/preview/6c245af8bcc075c4/42820335d3df842c) 30 | 欢迎各位flutter同学多多PR, 31 | 如果需要添加接口, 请邮箱联系我: ws156858@163.com 32 | 33 | ## 4 数据库访问 34 | MySQL数据库: 101.42.134.18:3306 35 | 账号: guest 36 | 密码: 123456 37 | > 注意: 38 | 本数据库账号仅有读权限, 所有写操作均通过`Http`或`Websocket`接口实现 39 | 40 | ![13456e16319b7ba652f4374297f9dce6.png](https://img.gejiba.com/images/13456e16319b7ba652f4374297f9dce6.png) 41 | 42 | 43 | ## 5 接下来的开发计划 44 | + [ ] 添加好友 45 | + [ ] 同意好友申请 46 | + [ ] 聊天页上拉自动获取历史消息 47 | + [ ] 修改密码 48 | + [ ] 修改用户信息 49 | + [ ] 消息页按最后时间倒序 50 | + [ ] 前端发送消息时websocket断线重连 51 | + [ ] 重连后拉取离线消息 52 | + [ ] 支持发送图片和视频 53 | 54 | 空闲时间会继续更新, 直至实现微信的大部分功能, 也欢迎各位大佬PR~ -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | analyzer: 13 | enable-experiment: 14 | - extension-methods 15 | 16 | linter: 17 | # The lint rules applied to this project can be customized in the 18 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 19 | # included above or to enable additional rules. A list of all available lints 20 | # and their documentation is published at 21 | # https://dart-lang.github.io/linter/lints/index.html. 22 | # 23 | # Instead of disabling a lint rule for the entire project in the 24 | # section below, it can also be suppressed for a single line of code 25 | # or a specific dart file by using the `// ignore: name_of_lint` and 26 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 27 | # producing the lint. 28 | rules: 29 | must_be_immutable: false 30 | prefer_const_constructors: false 31 | constant_identifier_names: false 32 | unnecessary_this: false 33 | avoid_print: false 34 | prefer_const_constructors_in_immutables: false 35 | prefer_const_literals_to_create_immutables: false 36 | lowercase_with_underscores: false 37 | annotate_overrides: false 38 | overridden_fields: false 39 | prefer_final_fields: false 40 | non_constant_identifier_names: false 41 | 42 | # Additional information about this file can be found at 43 | # https://dart.dev/guides/language/analysis-options 44 | -------------------------------------------------------------------------------- /assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /assets/images/addnew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/addnew.png -------------------------------------------------------------------------------- /assets/images/avator1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/avator1.jpg -------------------------------------------------------------------------------- /assets/images/avator2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/avator2.jpg -------------------------------------------------------------------------------- /assets/images/avator3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/avator3.jpg -------------------------------------------------------------------------------- /assets/images/avator4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/avator4.jpg -------------------------------------------------------------------------------- /assets/images/avator5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/avator5.jpg -------------------------------------------------------------------------------- /assets/images/cardpackage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/cardpackage.png -------------------------------------------------------------------------------- /assets/images/collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/collect.png -------------------------------------------------------------------------------- /assets/images/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/contact.png -------------------------------------------------------------------------------- /assets/images/emoij.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/emoij.png -------------------------------------------------------------------------------- /assets/images/file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/file.jpg -------------------------------------------------------------------------------- /assets/images/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/find.png -------------------------------------------------------------------------------- /assets/images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/group.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/label.png -------------------------------------------------------------------------------- /assets/images/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/message.png -------------------------------------------------------------------------------- /assets/images/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/mine.png -------------------------------------------------------------------------------- /assets/images/miniprogram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/miniprogram.png -------------------------------------------------------------------------------- /assets/images/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/more.png -------------------------------------------------------------------------------- /assets/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/next.png -------------------------------------------------------------------------------- /assets/images/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/picture.png -------------------------------------------------------------------------------- /assets/images/public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/public.png -------------------------------------------------------------------------------- /assets/images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/qrcode.png -------------------------------------------------------------------------------- /assets/images/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/scan.png -------------------------------------------------------------------------------- /assets/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/search.png -------------------------------------------------------------------------------- /assets/images/searchs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/searchs.png -------------------------------------------------------------------------------- /assets/images/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/setting.png -------------------------------------------------------------------------------- /assets/images/shake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/shake.png -------------------------------------------------------------------------------- /assets/images/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/video.png -------------------------------------------------------------------------------- /assets/images/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/view.png -------------------------------------------------------------------------------- /assets/images/wechat_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/wechat_moment.png -------------------------------------------------------------------------------- /assets/images/wxpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stdeson/wechat_flutter/5fd03c4ad0dfdf91eb61d6aad1c460b80d78e97d/assets/images/wxpay.png -------------------------------------------------------------------------------- /lib/common/apis/group.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:wechat_flutter/common/entities/index.dart'; 3 | import 'package:wechat_flutter/common/store/user.dart'; 4 | import 'package:wechat_flutter/common/xerr/index.dart'; 5 | import 'package:wechat_flutter/common/xresp/xresp.dart'; 6 | import 'package:wechat_flutter/common/utils/http.dart'; 7 | 8 | // 群组接口 9 | class GroupAPI { 10 | static final _httpClient = HttpClient("http://101.42.134.18:10003"); 11 | // 添加好友 12 | static Future register({AddFriendRequest? data}) async { 13 | var respData = await _httpClient.post( 14 | '/api/group/add_friend', 15 | data: data?.toJson(), 16 | options: Options(headers: {"Authorization": UserStore.to.token}), 17 | ); 18 | var respBody = RespBody.fromJson(respData); 19 | if (respBody.code == Status.OK) { 20 | return AddFriendResponse.fromJson(respBody.data); 21 | } 22 | throw respBody; 23 | } 24 | 25 | // 处理好友请求 26 | static Future handleFriend( 27 | {HandleFriendRequest? data}) async { 28 | var respData = await _httpClient.post( 29 | '/api/group/handle_friend', 30 | data: data?.toJson(), 31 | options: Options(headers: {"Authorization": UserStore.to.token}), 32 | ); 33 | var respBody = RespBody.fromJson(respData); 34 | if (respBody.code == Status.OK) { 35 | return HandleFriendResponse.fromJson(respBody.data); 36 | } 37 | throw respBody; 38 | } 39 | 40 | // 获取组内所有用户id 41 | static Future groupUserList( 42 | {GroupUserListRequest? data}) async { 43 | var respData = await _httpClient.post( 44 | '/api/group/group_user_list', 45 | data: data?.toJson(), 46 | options: Options(headers: {"Authorization": UserStore.to.token}), 47 | ); 48 | var respBody = RespBody.fromJson(respData); 49 | if (respBody.code == Status.OK) { 50 | return GroupUserListResponse.fromJson(respBody.data); 51 | } 52 | throw respBody; 53 | } 54 | 55 | // 消息页 群组信息列表 56 | static Future messageGroupInfoList( 57 | MessageGroupInfoListRequest? data) async { 58 | var respData = await _httpClient.post( 59 | '/api/group/message_group_info_list', 60 | data: data?.toJson(), 61 | options: Options(headers: {"Authorization": UserStore.to.token}), 62 | ); 63 | var respBody = RespBody.fromJson(respData); 64 | if (respBody.code == Status.OK) { 65 | return MessageGroupInfoListResponse.fromJson(respBody.data); 66 | } 67 | throw respBody; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/common/apis/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:wechat_flutter/common/entities/index.dart'; 3 | import 'package:wechat_flutter/common/store/user.dart'; 4 | import 'package:wechat_flutter/common/xerr/index.dart'; 5 | import 'package:wechat_flutter/common/xresp/xresp.dart'; 6 | import 'package:wechat_flutter/common/utils/http.dart'; 7 | 8 | // 用户接口 9 | class MessageAPI { 10 | static final _httpClient = HttpClient("http://101.42.134.18:10002"); 11 | // 上传 12 | static Future upload(UploadRequest? data) async { 13 | var respData = await _httpClient.post( 14 | '/api/message/upload', 15 | data: data?.toJson(), 16 | options: Options(headers: {"Authorization": UserStore.to.token}), 17 | ); 18 | var respBody = RespBody.fromJson(respData); 19 | if (respBody.code == Status.OK) { 20 | return UploadResponse.fromJson(respBody.data); 21 | } 22 | throw respBody; 23 | } 24 | 25 | // 拉取 26 | static Future pull(PullRequest? data) async { 27 | var respData = await _httpClient.post( 28 | '/api/message/pull', 29 | data: data?.toJson(), 30 | options: Options(headers: {"Authorization": UserStore.to.token}), 31 | ); 32 | var respBody = RespBody.fromJson(respData); 33 | if (respBody.code == Status.OK) { 34 | return PullResponse.fromJson(respBody.data); 35 | } 36 | throw respBody; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/apis/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:wechat_flutter/common/entities/index.dart'; 3 | import 'package:wechat_flutter/common/store/user.dart'; 4 | import 'package:wechat_flutter/common/xerr/index.dart'; 5 | import 'package:wechat_flutter/common/xresp/xresp.dart'; 6 | import 'package:wechat_flutter/common/utils/http.dart'; 7 | 8 | // 用户接口 9 | class UserAPI { 10 | static final _httpClient = HttpClient("http://101.42.134.18:10001"); 11 | // 注册 12 | static Future register({RegisterRequest? data}) async { 13 | var respData = await _httpClient.post( 14 | '/api/user/register', 15 | data: data?.toJson(), 16 | ); 17 | var respBody = RespBody.fromJson(respData); 18 | if (respBody.code == Status.OK) { 19 | return RegisterResponse.fromJson(respBody.data); 20 | } 21 | throw respBody; 22 | } 23 | 24 | // 登录 25 | static Future login({LoginRequest? data}) async { 26 | var respData = await _httpClient.post( 27 | '/api/user/login', 28 | data: data?.toJson(), 29 | ); 30 | var respBody = RespBody.fromJson(respData); 31 | if (respBody.code == Status.OK) { 32 | return LoginResponse.fromJson(respBody.data); 33 | } 34 | throw respBody; 35 | } 36 | 37 | // 获取个人信息 38 | static Future personalInfo( 39 | {PersonalInfoRequest? data}) async { 40 | var respData = await _httpClient.post( 41 | '/api/user/personal_info', 42 | data: data?.toJson(), 43 | options: Options(headers: {"Authorization": UserStore.to.token}), 44 | ); 45 | var respBody = RespBody.fromJson(respData); 46 | if (respBody.code == Status.OK) { 47 | return PersonalInfoResponse.fromJson(respBody.data); 48 | } 49 | throw respBody; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/common/biz/custom_class.dart: -------------------------------------------------------------------------------- 1 | import '../entities/message.dart'; 2 | 3 | extension MsgList on List { 4 | // 扩展List的方法 5 | int append(ChatMsg msg, [int startPos = -1]) { 6 | if (startPos == -1) { 7 | startPos = this.length - 1; // 从哪里开始遍历 8 | } 9 | for (var i = startPos; i >= 0; i--) { 10 | var cur = this[i]; 11 | // 自动去重 12 | if (cur.createTime == msg.createTime) { 13 | if (cur.uuid != msg.uuid) { 14 | this.insert(i + 1, msg); 15 | } 16 | return i; 17 | } 18 | // 从后往前遍历 找到第一个创建时间小于msg的createtime的 19 | if (cur.createTime < msg.createTime) { 20 | this.insert(i + 1, msg); 21 | return i; 22 | } 23 | // 如果是最后一个,直接插入 24 | if (i == 0) { 25 | this.insert(0, msg); 26 | return 0; 27 | } 28 | } 29 | return -1; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/common/biz/websocket.dart: -------------------------------------------------------------------------------- 1 | import 'package:web_socket_channel/io.dart'; 2 | 3 | typedef OnListen = void Function(dynamic); 4 | typedef OnError = Function; 5 | 6 | class WsSocket { 7 | late IOWebSocketChannel _channel; 8 | static const _URL = "ws://101.42.134.18:10002/ws"; 9 | final Map? headers; 10 | OnListen? onListen; 11 | OnError onError; 12 | 13 | WsSocket({this.headers, this.onListen, required this.onError}); 14 | 15 | // 开始监听 16 | start() { 17 | _channel = IOWebSocketChannel.connect(_URL, headers: headers); 18 | _channel.stream.listen(onListen); 19 | _channel.stream.handleError(onError); 20 | } 21 | 22 | // 向服务端发送消息 23 | send(String message) { 24 | _channel.sink.add(message); 25 | } 26 | 27 | // 关闭连接 28 | close() { 29 | _channel.sink.close(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/common/entities/group.dart: -------------------------------------------------------------------------------- 1 | // --./group-- 2 | 3 | import 'message.dart'; 4 | 5 | class AddFriendRequest { 6 | final int userId; 7 | AddFriendRequest({ 8 | required this.userId, 9 | }); 10 | factory AddFriendRequest.fromJson(Map m) { 11 | return AddFriendRequest( 12 | userId: m['user_id'], 13 | ); 14 | } 15 | Map toJson() { 16 | return { 17 | 'user_id': userId, 18 | }; 19 | } 20 | } 21 | 22 | class AddFriendResponse { 23 | AddFriendResponse(); 24 | factory AddFriendResponse.fromJson(Map m) { 25 | return AddFriendResponse(); 26 | } 27 | Map toJson() { 28 | return {}; 29 | } 30 | } 31 | 32 | class HandleFriendRequest { 33 | final String groupId; 34 | 35 | final bool isAgree; 36 | HandleFriendRequest({ 37 | required this.groupId, 38 | required this.isAgree, 39 | }); 40 | factory HandleFriendRequest.fromJson(Map m) { 41 | return HandleFriendRequest( 42 | groupId: m['groupId'], 43 | isAgree: m['isAgree'], 44 | ); 45 | } 46 | Map toJson() { 47 | return { 48 | 'groupId': groupId, 49 | 'isAgree': isAgree, 50 | }; 51 | } 52 | } 53 | 54 | class HandleFriendResponse { 55 | final String groupId; 56 | HandleFriendResponse({ 57 | required this.groupId, 58 | }); 59 | factory HandleFriendResponse.fromJson(Map m) { 60 | return HandleFriendResponse( 61 | groupId: m['groupId'], 62 | ); 63 | } 64 | Map toJson() { 65 | return { 66 | 'groupId': groupId, 67 | }; 68 | } 69 | } 70 | 71 | class GroupUserListRequest { 72 | final String groupId; 73 | GroupUserListRequest({ 74 | required this.groupId, 75 | }); 76 | factory GroupUserListRequest.fromJson(Map m) { 77 | return GroupUserListRequest( 78 | groupId: m['groupId'], 79 | ); 80 | } 81 | Map toJson() { 82 | return { 83 | 'groupId': groupId, 84 | }; 85 | } 86 | } 87 | 88 | class GroupUserListResponse { 89 | final List list; 90 | GroupUserListResponse({ 91 | required this.list, 92 | }); 93 | factory GroupUserListResponse.fromJson(Map m) { 94 | return GroupUserListResponse( 95 | list: m['list'], 96 | ); 97 | } 98 | Map toJson() { 99 | return { 100 | 'list': list, 101 | }; 102 | } 103 | } 104 | 105 | class MessageGroupInfoListRequest { 106 | MessageGroupInfoListRequest(); 107 | factory MessageGroupInfoListRequest.fromJson(Map m) { 108 | return MessageGroupInfoListRequest(); 109 | } 110 | Map toJson() { 111 | return {}; 112 | } 113 | } 114 | 115 | 116 | class MessageGroupInfo { 117 | final String groupId; 118 | 119 | final String aliasName; 120 | 121 | final String avatarUrl; 122 | 123 | final ChatMsg lastMsg; 124 | MessageGroupInfo({ 125 | required this.groupId, 126 | required this.aliasName, 127 | required this.avatarUrl, 128 | required this.lastMsg, 129 | }); 130 | factory MessageGroupInfo.fromJson(Map m) { 131 | return MessageGroupInfo( 132 | groupId: m['groupId'], 133 | aliasName: m['aliasName'], 134 | avatarUrl: m['avatarUrl'], 135 | lastMsg: ChatMsg.fromJson(m['lastMsg']), 136 | ); 137 | } 138 | Map toJson() { 139 | return { 140 | 'groupId': groupId, 141 | 'aliasName': aliasName, 142 | 'avatarUrl': avatarUrl, 143 | 'lastMsg': lastMsg.toJson(), 144 | }; 145 | } 146 | } 147 | 148 | class MessageGroupInfoListResponse { 149 | final List list; 150 | MessageGroupInfoListResponse({ 151 | required this.list, 152 | }); 153 | factory MessageGroupInfoListResponse.fromJson(Map m) { 154 | return MessageGroupInfoListResponse( 155 | list: (m['list'] as List) 156 | .map((i) => MessageGroupInfo.fromJson(i)) 157 | .toList(), 158 | ); 159 | } 160 | Map toJson() { 161 | return { 162 | 'list': list.map((i) => i.toJson()), 163 | }; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/common/entities/index.dart: -------------------------------------------------------------------------------- 1 | export './user.dart'; 2 | export './group.dart'; 3 | export './message.dart'; 4 | -------------------------------------------------------------------------------- /lib/common/entities/message.dart: -------------------------------------------------------------------------------- 1 | // --./message-- 2 | 3 | class UploadRequest { 4 | final String groupId; 5 | 6 | final int type; 7 | 8 | final String content; 9 | 10 | final String uuid; 11 | UploadRequest({ 12 | required this.groupId, 13 | required this.type, 14 | required this.content, 15 | required this.uuid, 16 | }); 17 | factory UploadRequest.fromJson(Map m) { 18 | return UploadRequest( 19 | groupId: m['groupId'], 20 | type: m['type'], 21 | content: m['content'], 22 | uuid: m['uuid'], 23 | ); 24 | } 25 | Map toJson() { 26 | return { 27 | 'groupId': groupId, 28 | 'type': type, 29 | 'content': content, 30 | 'uuid': uuid, 31 | }; 32 | } 33 | } 34 | 35 | class UploadResponse { 36 | final int id; 37 | 38 | final int createTime; 39 | UploadResponse({ 40 | required this.id, 41 | required this.createTime, 42 | }); 43 | factory UploadResponse.fromJson(Map m) { 44 | return UploadResponse( 45 | id: m['id'], 46 | createTime: m['createTime'], 47 | ); 48 | } 49 | Map toJson() { 50 | return { 51 | 'id': id, 52 | 'createTime': createTime, 53 | }; 54 | } 55 | } 56 | 57 | class PullRequest { 58 | final String platform; 59 | 60 | final String groupId; 61 | 62 | final int maxMsgId; 63 | PullRequest({ 64 | required this.platform, 65 | required this.groupId, 66 | required this.maxMsgId, 67 | }); 68 | factory PullRequest.fromJson(Map m) { 69 | return PullRequest( 70 | platform: m['platform'], 71 | groupId: m['groupId'], 72 | maxMsgId: m['maxMsgId'], 73 | ); 74 | } 75 | Map toJson() { 76 | return { 77 | 'platform': platform, 78 | 'groupId': groupId, 79 | 'maxMsgId': maxMsgId, 80 | }; 81 | } 82 | } 83 | 84 | class ChatMsg { 85 | int id; 86 | 87 | final String groupId; 88 | 89 | final int senderId; 90 | 91 | final int type; 92 | 93 | final String content; 94 | 95 | final String uuid; 96 | 97 | final int createTime; 98 | ChatMsg({ 99 | this.id = 0, 100 | required this.groupId, 101 | required this.senderId, 102 | required this.type, 103 | required this.content, 104 | required this.uuid, 105 | required this.createTime, 106 | }); 107 | factory ChatMsg.fromJson(Map m) { 108 | return ChatMsg( 109 | id: m['id'], 110 | groupId: m['groupId'], 111 | senderId: m['senderId'], 112 | type: m['type'], 113 | content: m['content'], 114 | uuid: m['uuid'], 115 | createTime: m['createTime'], 116 | ); 117 | } 118 | Map toJson() { 119 | return { 120 | 'id': id, 121 | 'groupId': groupId, 122 | 'senderId': senderId, 123 | 'type': type, 124 | 'content': content, 125 | 'uuid': uuid, 126 | 'createTime': createTime, 127 | }; 128 | } 129 | } 130 | 131 | class PullResponse { 132 | final List list; 133 | PullResponse({ 134 | required this.list, 135 | }); 136 | factory PullResponse.fromJson(Map m) { 137 | return (m['list'] == null) 138 | ? PullResponse(list: []) 139 | : PullResponse( 140 | list: (m['list'] as List) 141 | .map((i) => ChatMsg.fromJson(i)) 142 | .toList(), 143 | ); 144 | } 145 | Map toJson() { 146 | return { 147 | 'list': list.map((i) => i.toJson()), 148 | }; 149 | } 150 | } 151 | 152 | class GroupInfo { 153 | final String groupId; 154 | final String avatarUrl; 155 | final String aliasName; 156 | 157 | GroupInfo( 158 | {required this.groupId, 159 | required this.avatarUrl, 160 | required this.aliasName}); 161 | 162 | factory GroupInfo.fromJson(Map m) { 163 | return GroupInfo( 164 | groupId: m['groupId'], 165 | avatarUrl: m['avatarUrl'], 166 | aliasName: m['aliasName'], 167 | ); 168 | } 169 | Map toJson() { 170 | return { 171 | 'groupId': groupId, 172 | 'avatarUrl': avatarUrl, 173 | 'aliasName': aliasName, 174 | }; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/common/entities/user.dart: -------------------------------------------------------------------------------- 1 | class RegisterRequest { 2 | final String email; 3 | final String password; 4 | String? nickName; 5 | int? gender; 6 | RegisterRequest({ 7 | required this.email, 8 | required this.password, 9 | this.nickName, 10 | this.gender, 11 | }); 12 | factory RegisterRequest.fromJson(Map m) { 13 | return RegisterRequest( 14 | email: m['email'], 15 | password: m['password'], 16 | nickName: m['nickName'], 17 | gender: m['gender'], 18 | ); 19 | } 20 | Map toJson() { 21 | return { 22 | 'email': email, 23 | 'password': password, 24 | 'nickName': nickName, 25 | 'gender': gender, 26 | }; 27 | } 28 | } 29 | 30 | class RegisterResponse { 31 | RegisterResponse(); 32 | factory RegisterResponse.fromJson(Map? m) { 33 | return RegisterResponse(); 34 | } 35 | Map toJson() { 36 | return {}; 37 | } 38 | } 39 | 40 | class LoginRequest { 41 | final String email; 42 | final String password; 43 | LoginRequest({ 44 | required this.email, 45 | required this.password, 46 | }); 47 | factory LoginRequest.fromJson(Map m) { 48 | return LoginRequest( 49 | email: m['email'], 50 | password: m['password'], 51 | ); 52 | } 53 | Map toJson() { 54 | return { 55 | 'email': email, 56 | 'password': password, 57 | }; 58 | } 59 | } 60 | 61 | class LoginResponse { 62 | final String accessToken; 63 | final int accessExpire; 64 | LoginResponse({ 65 | required this.accessToken, 66 | required this.accessExpire, 67 | }); 68 | factory LoginResponse.fromJson(Map m) { 69 | return LoginResponse( 70 | accessToken: m['accessToken'], 71 | accessExpire: m['accessExpire'], 72 | ); 73 | } 74 | Map toJson() { 75 | return { 76 | 'accessToken': accessToken, 77 | 'accessExpire': accessExpire, 78 | }; 79 | } 80 | } 81 | 82 | class PersonalInfoRequest { 83 | PersonalInfoRequest(); 84 | factory PersonalInfoRequest.fromJson(Map m) { 85 | return PersonalInfoRequest(); 86 | } 87 | Map toJson() { 88 | return {}; 89 | } 90 | } 91 | 92 | class PersonalInfoResponse { 93 | final int userId; 94 | final String nickName; 95 | final int gender; 96 | final String email; 97 | final String avatarUrl; 98 | PersonalInfoResponse({ 99 | required this.userId, 100 | required this.nickName, 101 | required this.gender, 102 | required this.email, 103 | required this.avatarUrl 104 | }); 105 | factory PersonalInfoResponse.fromJson(Map m) { 106 | return PersonalInfoResponse( 107 | userId: m['userId'], 108 | nickName: m['nickName'], 109 | gender: m['gender'], 110 | email: m['email'], 111 | avatarUrl: m['avatarUrl'], 112 | ); 113 | } 114 | Map toJson() { 115 | return { 116 | 'userId': userId, 117 | 'nickName': nickName, 118 | 'gender': gender, 119 | 'email': email, 120 | 'avatarUrl': avatarUrl, 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/common/langs/en_US.dart: -------------------------------------------------------------------------------- 1 | const Map en_us = { 2 | 'login_title': 'WeChat', 3 | 'login_email': 'email', 4 | 'login_email_hint': 'xxx@gmail.com', 5 | }; 6 | -------------------------------------------------------------------------------- /lib/common/langs/translation_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'zh_hans.dart'; 4 | import 'en_us.dart'; 5 | 6 | class TranslationService extends Translations { 7 | static Locale? get locale => Get.deviceLocale; 8 | static final fallbackLocale = Locale('en', 'US'); 9 | @override 10 | Map> get keys => { 11 | 'en_US': en_us, 12 | 'zh_Hans': zh_hans, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /lib/common/langs/zh_Hans.dart: -------------------------------------------------------------------------------- 1 | const Map zh_hans = { 2 | 'login_title': '微 信', 3 | 'login_email': '电子邮箱', 4 | 'login_email_hint': 'xxx@foxmail.com', 5 | 'login_password': "密码", 6 | 'login_password_hint': "6~12位字母数字", 7 | 'login_button_login': "登 录", 8 | 'login_button_register': "注 册", 9 | 10 | "login_err_title": "登录失败", 11 | "register_err_title": "注册失败", 12 | 13 | "personal_err_title": "获取个人信息失败", 14 | }; 15 | -------------------------------------------------------------------------------- /lib/common/middlewares/auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/routes/routes.dart'; 4 | import 'package:wechat_flutter/common/store/user.dart'; 5 | 6 | // 需要登录 7 | class LoginRequired extends GetMiddleware { 8 | int? priority = 0; 9 | 10 | LoginRequired([this.priority]); 11 | 12 | @override 13 | RouteSettings? redirect(String? route) { 14 | // 如果获取到了token, 则 15 | if (UserStore.to.token != "") { 16 | return null; 17 | } 18 | Future.delayed( 19 | Duration(seconds: 1), () => Get.snackbar("提示", "登录过期,请重新登录")); 20 | return RouteSettings(name: AppRouter.Login); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/common/mock/data.dart: -------------------------------------------------------------------------------- 1 | import 'package:wechat_flutter/common/entities/index.dart'; 2 | 3 | class Data { 4 | static var mockGroupInfoMap = { 5 | "39_40": { 6 | "aliasName": "flutter", 7 | "avatarUrl": "https://s1.ax1x.com/2022/05/19/Oq3yDg.jpg", 8 | }, 9 | "39_41": { 10 | "aliasName": "python", 11 | "avatarUrl": "https://s1.ax1x.com/2022/05/19/Oq3fCq.jpg", 12 | }, 13 | "39_42": { 14 | "aliasName": "golang", 15 | "avatarUrl": "https://s1.ax1x.com/2022/05/19/Oq3h80.jpg", 16 | }, 17 | "39_43": { 18 | "aliasName": "ruby", 19 | "avatarUrl": "https://s1.ax1x.com/2022/05/19/Oq3TrF.jpg", 20 | }, 21 | "39_44": { 22 | "aliasName": "C++", 23 | "avatarUrl": "https://s1.ax1x.com/2022/05/19/Oq8API.jpg", 24 | }, 25 | }; 26 | 27 | // 消息页 28 | static List mockMessageList = [ 29 | ChatMsg( 30 | groupId: "39_40", 31 | senderId: 1, 32 | type: 1, 33 | content: "一套代码支持全平台", 34 | uuid: "123", 35 | createTime: 1652973045000, 36 | ), 37 | ChatMsg( 38 | groupId: "39_41", 39 | senderId: 2, 40 | type: 1, 41 | content: "hello world", 42 | uuid: "123", 43 | createTime: 1652973046000, 44 | ), 45 | ChatMsg( 46 | groupId: "39_42", 47 | senderId: 3, 48 | type: 2, 49 | content: "[图片]", 50 | uuid: "123", 51 | createTime: 1652973047000, 52 | ), 53 | ChatMsg( 54 | groupId: "39_43", 55 | senderId: 4, 56 | type: 1, 57 | content: "通过通信来共享内存, 而非共享内存来通信", 58 | uuid: "123", 59 | createTime: 1652973048000, 60 | ), 61 | ChatMsg( 62 | groupId: "39_44", 63 | senderId: 5, 64 | type: 1, 65 | content: "欢迎来到微信", 66 | uuid: "123", 67 | createTime: 1652973049000, 68 | ), 69 | ]; 70 | 71 | // 聊天详情 72 | static List mockchatMessageList = [ 73 | ChatMsg( 74 | groupId: "39_40", 75 | senderId: 1, 76 | type: 1, 77 | content: "一套代码支持全平台", 78 | uuid: "123", 79 | createTime: 1652973045000, 80 | ), 81 | ChatMsg( 82 | groupId: "39_41", 83 | senderId: 2, 84 | type: 1, 85 | content: "hello world", 86 | uuid: "123", 87 | createTime: 1652973046000, 88 | ), 89 | ChatMsg( 90 | groupId: "39_41", 91 | senderId: 3, 92 | type: 2, 93 | content: "[图片]", 94 | uuid: "123", 95 | createTime: 1652973047000, 96 | ), 97 | ChatMsg( 98 | groupId: "39_41", 99 | senderId: 4, 100 | type: 1, 101 | content: "通过通信来共享内存, 而非共享内存来通信", 102 | uuid: "123", 103 | createTime: 1652973048000, 104 | ), 105 | ChatMsg( 106 | groupId: "39_41", 107 | senderId: 5, 108 | type: 1, 109 | content: "欢迎来到微信", 110 | uuid: "123", 111 | createTime: 1652973049000, 112 | ), 113 | ]; 114 | 115 | static Map contact = { 116 | "first": [], 117 | "G": [ 118 | { 119 | "avator": "assets/images/avator3.jpg", 120 | "nickName": "Golang", 121 | "pinyin": "golang" 122 | } 123 | ], 124 | "F": [ 125 | { 126 | "avator": "assets/images/avator1.jpg", 127 | "nickName": "Flutter", 128 | "pinyin": "flutter" 129 | } 130 | ], 131 | "P": [ 132 | { 133 | "avator": "assets/images/avator2.jpg", 134 | "nickName": "Python", 135 | "pinyin": "python" 136 | } 137 | ], 138 | "W": [ 139 | { 140 | "avator": "assets/images/file.jpg", 141 | "nickName": "文件传输助手", 142 | "pinyin": "wenjianchuanshuzhushou" 143 | }, 144 | { 145 | "avator": "assets/images/icon.png", 146 | "nickName": "微信团队", 147 | "pinyin": "weixintuandui" 148 | }, 149 | ], 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /lib/common/routes/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:wechat_flutter/common/middlewares/auth.dart'; 3 | import 'package:wechat_flutter/pages/chat/index.dart'; 4 | import 'package:wechat_flutter/pages/discover/index.dart'; 5 | import 'package:wechat_flutter/pages/friend/index.dart'; 6 | import 'package:wechat_flutter/pages/message/index.dart'; 7 | import 'package:wechat_flutter/pages/mine/index.dart'; 8 | import 'package:wechat_flutter/pages/frame/login/bindings.dart'; 9 | import 'package:wechat_flutter/pages/frame/login/view.dart'; 10 | import 'package:wechat_flutter/pages/home/index.dart'; 11 | import 'package:wechat_flutter/pages/frame/notfound/index.dart'; 12 | 13 | class AppRouter { 14 | // frame 15 | static const Login = '/login'; // 登录页 16 | static const NotFound = '/notfound'; // 未知页 17 | // home 18 | static const Home = '/home'; // 首页 19 | static const Message = '/message'; // 消息 20 | static const Friend = '/friend'; // 好友 21 | static const Discover = '/discover'; // 发现 22 | static const Mine = '/mine'; // 我的 23 | static const Chat = '/chat'; // 聊天 24 | // 初始路由 25 | static const INITIAL = Home; 26 | // 未知路由 27 | static final unknownRoute = 28 | GetPage(name: NotFound, page: () => NotFoundView()); 29 | // 路由 30 | static final routes = [ 31 | GetPage( 32 | name: Login, 33 | page: () => LoginPage(), 34 | binding: LoginBinding(), 35 | ), 36 | GetPage( 37 | name: Home, 38 | page: () => HomePage(), 39 | binding: HomeBinding(), 40 | middlewares: [LoginRequired()], 41 | ), 42 | GetPage( 43 | name: Message, 44 | page: () => MessagePage(), 45 | binding: MessageBinding(), 46 | middlewares: [LoginRequired()], 47 | ), 48 | GetPage( 49 | name: Friend, 50 | page: () => FriendPage(), 51 | binding: FriendBinding(), 52 | middlewares: [LoginRequired()], 53 | ), 54 | GetPage( 55 | name: Discover, 56 | page: () => DiscoverPage(), 57 | binding: DiscoverBinding(), 58 | middlewares: [LoginRequired()], 59 | ), 60 | GetPage( 61 | name: Mine, 62 | page: () => MinePage(), 63 | binding: MineBinding(), 64 | middlewares: [LoginRequired()], 65 | ), 66 | GetPage( 67 | name: Chat, 68 | page: () => ChatPage(tag: '',), 69 | binding: ChatBinding(), 70 | middlewares: [LoginRequired()], 71 | ), 72 | ]; 73 | } 74 | -------------------------------------------------------------------------------- /lib/common/store/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:wechat_flutter/common/utils/storage.dart'; 3 | import 'package:wechat_flutter/common/values/storage.dart'; 4 | 5 | // 用户信息存储到本地 6 | class UserStore extends GetxController { 7 | static UserStore get to => Get.find(); 8 | // token 9 | String token = ''; 10 | bool get hasToken => token.isNotEmpty; 11 | 12 | @override 13 | void onInit() { 14 | super.onInit(); 15 | this.token = StorageService.to.getString(STORAGE_USER_TOKEN_KEY); 16 | } 17 | 18 | // 保存 token 19 | Future setToken(String value) async { 20 | await StorageService.to.setString(STORAGE_USER_TOKEN_KEY, value); 21 | this.token = value; 22 | } 23 | 24 | // 删除 token 25 | Future rmToken() async { 26 | await StorageService.to.remove(STORAGE_USER_TOKEN_KEY); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/common/style/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | static const APPBarColor = 0xffededed; 5 | static const APPBarTextColor = 0xff010101; 6 | static const APPCardColor = 0xff303030; 7 | static const TabIconNormal = 0xff999999; 8 | static const TabIconActive = 0xff46c11b; 9 | static const AppBarPopupMenuTextColor = 0xffffffff; 10 | static const TitleColor = 0xff353535; 11 | static const ConversationItemBg = 0xffffffff; 12 | static const DesTextColor = 0xff9e9e9e; 13 | static const DividerColor = 0xffd9d9d9; 14 | static const NotifyDotBg = 0xffff3e3e; 15 | static const NotifyDotText = 0xffffffff; 16 | static const ConversationMuteIcon = 0xffd8d8d8; 17 | static const DeviceInfoItemBg = 0xfff5f5f5; 18 | static const DeviceInfoItemText = 0xff606062; 19 | static const PrimaryColor = 0xffebebeb; 20 | static const BackgroundColor = 0xffededed; 21 | static const AppBarColor = 0xffededed; 22 | static const ActionIconColor = 0xff000000; 23 | static const ActionMenuBgColor = 0xff4c4c4c; 24 | static const CardBgColor = 0xffffffff; 25 | static const AppBarPopupMenuColor = 0xffffffff; 26 | static const DeviceInfoItemIcon = 0xff606062; 27 | static const ContactGroupTitleBg = 0xffebebeb; 28 | static const ContactGroupTitleText = 0xff888888; 29 | static const IndexLetterBoxBg = Colors.black45; 30 | static const HeaderCardBg = Colors.white; 31 | static const HeaderCardTitleText = 0xff353535; 32 | static const HeaderCardDesText = 0xff7f7f7f; 33 | static const ButtonDesText = 0xff8c8c8c; 34 | static const ButtonArrowColor = 0xffadadad; 35 | static const NewTagBg = 0xfffa5251; 36 | static const FullWithIconButton = 0xff3d3d3d; 37 | static const KeyboardArrowRight = 0xffacacac; 38 | static const TextBobuleRight = 0xff9def71; 39 | static const TextBobuleLeft = 0xffffffff; 40 | static const TextBobule = 0xff3e3e3e; 41 | static const ChatDetailBg = 0xffefefef; 42 | static const ChatTime = 0xffababab; 43 | } 44 | -------------------------------------------------------------------------------- /lib/common/style/icons.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ICons { 4 | static const String FONT_FAMILY = 'wxIconFont'; 5 | 6 | static const IconData MESSAGE = 7 | IconData(0xe622, fontFamily: ICons.FONT_FAMILY); 8 | 9 | static const IconData ADDRESSLIST = 10 | IconData(0xe648, fontFamily: ICons.FONT_FAMILY); 11 | 12 | static const IconData DISCOVER = 13 | IconData(0xe613, fontFamily: ICons.FONT_FAMILY); 14 | 15 | static const IconData MINE = 16 | IconData(0xe670, fontFamily: ICons.FONT_FAMILY); 17 | 18 | static const IconData MESSAGE_ACTIVE = 19 | IconData(0xe620, fontFamily: ICons.FONT_FAMILY); 20 | 21 | static const IconData ADDRESSLIST_ACTIVE = 22 | IconData(0xe603, fontFamily: ICons.FONT_FAMILY); 23 | 24 | static const IconData DISCOVER_ACTIVE = 25 | IconData(0xe600, fontFamily: ICons.FONT_FAMILY); 26 | 27 | static const IconData MINE_ACTIVE = 28 | IconData(0xe601, fontFamily: ICons.FONT_FAMILY); 29 | 30 | static const IconData QR_SCAN = 31 | IconData(0xe634, fontFamily: ICons.FONT_FAMILY); 32 | 33 | static const IconData GROUP_CHAT = 34 | IconData(0xe620, fontFamily: ICons.FONT_FAMILY); 35 | 36 | static const IconData ADD_FRIEND = 37 | IconData(0xe624, fontFamily: ICons.FONT_FAMILY); 38 | 39 | static const IconData PAYMENT = 40 | IconData(0xe602, fontFamily: ICons.FONT_FAMILY); 41 | 42 | static const IconData HELP = 43 | IconData(0xe63b, fontFamily: ICons.FONT_FAMILY); 44 | 45 | static const IconData MUTE_ICON = 46 | IconData(0xe75e, fontFamily: ICons.FONT_FAMILY); 47 | 48 | static const IconData MAC = 49 | IconData(0xe673, fontFamily: ICons.FONT_FAMILY); 50 | 51 | static const IconData WINDOWS = 52 | IconData(0xe64f, fontFamily: ICons.FONT_FAMILY); 53 | 54 | static const IconData SEARCH = 55 | IconData(0xe63e, fontFamily: ICons.FONT_FAMILY); 56 | 57 | static const IconData ADD = 58 | IconData(0xe6d3, fontFamily: ICons.FONT_FAMILY); 59 | 60 | static const IconData ER_CODE = 61 | IconData(0xe646, fontFamily: ICons.FONT_FAMILY); 62 | 63 | static const IconData RIGHT = 64 | IconData(0xe60b, fontFamily: ICons.FONT_FAMILY); 65 | 66 | static const IconData MENUS = 67 | IconData(0xe60e, fontFamily: ICons.FONT_FAMILY); 68 | 69 | static const IconData FACES = 70 | IconData(0xe88f, fontFamily: ICons.FONT_FAMILY); 71 | 72 | static const IconData VOICE = 73 | IconData(0xe606, fontFamily: ICons.FONT_FAMILY); 74 | } 75 | -------------------------------------------------------------------------------- /lib/common/style/menus.dart: -------------------------------------------------------------------------------- 1 | class MessageDetailSelects { 2 | static const String MENU_COPY = 'MENU_COPY'; 3 | static const String MENU_COPY_VALUE = '复制'; 4 | static const String MENU_SHARE_FRIENDS = 'MENU_SHARE_FRIENDS'; 5 | static const String MENU_SHARE_FRIENDS_VALUE = '发送给朋友'; 6 | static const String MENU_FAVORIITE = 'MENU_MENU_FAVORIITE'; 7 | static const String MENU_FAVORIITE_VALUE = '收藏'; 8 | static const String MENU_REMIND = 'MENU_REMIND'; 9 | static const String MENU_REMIND_VALUE = '提醒'; 10 | static const String MENU_TRANSLATE = 'MENU_TRANSLATE'; 11 | static const String MENU_TRANSLATE_VALUE = '翻译'; 12 | static const String MENU_DELATE = 'MENU_DELATE'; 13 | static const String MENU_DELATE_VALUE = '删除'; 14 | static const String MENU_MULTIPE_CHOICE = 'MENU_MULTIPE_CHOICE'; 15 | static const String MENU_MULTIPE_CHOICE_VALUE = '多选'; 16 | } 17 | -------------------------------------------------------------------------------- /lib/common/utils/http.dart: -------------------------------------------------------------------------------- 1 | import 'package:cookie_jar/cookie_jar.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio_cookie_manager/dio_cookie_manager.dart'; 4 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 5 | 6 | import 'loading.dart'; // 避免和dio里的冲突 7 | 8 | // 是否启用缓存 9 | const CACHE_ENABLE = false; 10 | // 缓存的最长时间,单位(秒) 11 | const CACHE_MAXAGE = 1000; 12 | // 最大缓存数 13 | const CACHE_MAXCOUNT = 100; 14 | 15 | class HttpClient { 16 | late Dio dio; 17 | late String baseUrl; 18 | static CancelToken cancelToken = CancelToken(); 19 | 20 | HttpClient(this.baseUrl) { 21 | BaseOptions options = BaseOptions( 22 | baseUrl: this.baseUrl, 23 | // 连接服务器超时时间,单位是毫秒. 24 | connectTimeout: 10000, 25 | // 响应流上前后两次接受到数据的间隔,单位为毫秒。 26 | receiveTimeout: 5000, 27 | // Http请求头. 28 | headers: {}, 29 | // 请求体自动编码为哪种格式 30 | contentType: 'application/json; charset=utf-8', 31 | // 以那种格式(方式)接受响应数据, 目前接受三种类型 `JSON`, `STREAM`, `PLAIN`. 32 | responseType: ResponseType.json, 33 | ); 34 | dio = Dio(options); 35 | // Cookie管理 36 | CookieJar cookieJar = CookieJar(); 37 | // 添加拦截器 38 | dio.interceptors.add(CookieManager(cookieJar)); 39 | dio.interceptors.add(InterceptorsWrapper( 40 | // 请求拦截器 41 | onRequest: (options, handler) { 42 | return handler.next(options); //continue 43 | }, 44 | // 响应拦截器 45 | onResponse: (response, handler) { 46 | if (response.statusCode != 200) { 47 | handler.reject(DioError( 48 | requestOptions: RequestOptions(data: response.data, path: ''))); 49 | } 50 | return handler.next(response); // continue 51 | }, 52 | // 错误拦截器 53 | onError: (DioError e, handler) { 54 | Loading.dismiss(); 55 | ErrorEntity eInfo = createErrorEntity(e); 56 | onError(eInfo); 57 | return handler.next(e); //continue 58 | }, 59 | )); 60 | // 如果你想完成请求并返回一些自定义数据,你可以resolve一个Response对象 `handler.resolve(response)`。 61 | // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义response. 62 | 63 | // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,如`handler.reject(error)`, 64 | // 这样请求将被中止并触发异常,上层catchError会被调用。 65 | } 66 | 67 | // 错误处理 68 | void onError(ErrorEntity eInfo) { 69 | print('error.code -> ${eInfo.code}, error.message -> ${eInfo.message}'); 70 | switch (eInfo.code) { 71 | case 401: 72 | // UserStore.to.onLogout(); 73 | EasyLoading.showError(eInfo.message); 74 | break; 75 | default: 76 | EasyLoading.showError(eInfo.message); 77 | break; 78 | } 79 | } 80 | 81 | // 错误信息 82 | ErrorEntity createErrorEntity(DioError error) { 83 | switch (error.type) { 84 | case DioErrorType.cancel: 85 | return ErrorEntity(code: -1, message: "请求取消"); 86 | case DioErrorType.connectTimeout: 87 | return ErrorEntity(code: -1, message: "连接超时"); 88 | case DioErrorType.sendTimeout: 89 | return ErrorEntity(code: -1, message: "请求超时"); 90 | case DioErrorType.receiveTimeout: 91 | return ErrorEntity(code: -1, message: "响应超时"); 92 | case DioErrorType.response: 93 | { 94 | try { 95 | int errCode = 96 | error.response != null ? error.response!.statusCode! : -1; 97 | // String errMsg = error.response.statusMessage; 98 | // return ErrorEntity(code: errCode, message: errMsg); 99 | switch (errCode) { 100 | case 400: 101 | return ErrorEntity(code: errCode, message: "请求语法错误"); 102 | case 401: 103 | return ErrorEntity(code: errCode, message: "没有权限"); 104 | case 403: 105 | return ErrorEntity(code: errCode, message: "服务器拒绝执行"); 106 | case 404: 107 | return ErrorEntity(code: errCode, message: "无法连接服务器"); 108 | case 405: 109 | return ErrorEntity(code: errCode, message: "请求方法被禁止"); 110 | case 500: 111 | return ErrorEntity(code: errCode, message: "服务器内部错误"); 112 | case 502: 113 | return ErrorEntity(code: errCode, message: "无效的请求"); 114 | case 503: 115 | return ErrorEntity(code: errCode, message: "服务器挂了"); 116 | case 505: 117 | return ErrorEntity(code: errCode, message: "不支持HTTP协议请求"); 118 | default: 119 | { 120 | // return ErrorEntity(code: errCode, message: "未知错误"); 121 | return ErrorEntity( 122 | code: errCode, 123 | message: error.response != null 124 | ? error.response!.statusMessage! 125 | : "", 126 | ); 127 | } 128 | } 129 | } on Exception catch (_) { 130 | return ErrorEntity(code: -1, message: "未知错误"); 131 | } 132 | } 133 | default: 134 | { 135 | return ErrorEntity(code: -1, message: error.message); 136 | } 137 | } 138 | } 139 | 140 | /* 141 | * 取消请求 142 | * 143 | * 同一个cancel token 可以用于多个请求,当一个cancel token取消时,所有使用该cancel token的请求都会被取消。 144 | * 所以参数可选 145 | */ 146 | void cancelRequests(CancelToken token) { 147 | token.cancel("cancelled"); 148 | } 149 | 150 | // 读取本地配置 151 | Map? getAuthorizationHeader() { 152 | var headers = {}; 153 | // if (Get.isRegistered() && UserStore.to.hasToken == true) { 154 | // headers['Authorization'] = 'Bearer ${UserStore.to.token}'; 155 | // } 156 | return headers; 157 | } 158 | 159 | Future get( 160 | String path, { 161 | Map? queryParameters, 162 | Options? options, 163 | bool refresh = false, // 是否下拉刷新 164 | bool noCache = !CACHE_ENABLE, // 是否不缓存 165 | bool list = false, // 是否列表 166 | String cacheKey = '', // 缓存key 167 | bool cacheDisk = false, // 是否磁盘缓存 168 | }) async { 169 | Options requestOptions = options ?? Options(); 170 | 171 | requestOptions.extra ??= {}; // 若extra为null, 则设为空Map 172 | 173 | requestOptions.extra!.addAll({ 174 | "refresh": refresh, 175 | "noCache": noCache, 176 | "list": list, 177 | "cacheKey": cacheKey, 178 | "cacheDisk": cacheDisk, 179 | }); 180 | requestOptions.headers = requestOptions.headers ?? {}; 181 | Map? authorization = getAuthorizationHeader(); 182 | if (authorization != null) { 183 | requestOptions.headers!.addAll(authorization); 184 | } 185 | 186 | var response = await dio.get( 187 | path, 188 | queryParameters: queryParameters, 189 | options: options, 190 | cancelToken: cancelToken, 191 | ); 192 | return response.data; 193 | } 194 | 195 | Future post( 196 | String path, { 197 | dynamic data, 198 | Map? queryParameters, 199 | Options? options, 200 | }) async { 201 | Options requestOptions = options ?? Options(); 202 | requestOptions.headers = requestOptions.headers ?? {}; 203 | Map? authorization = getAuthorizationHeader(); 204 | if (authorization != null) { 205 | requestOptions.headers!.addAll(authorization); 206 | } 207 | var response = await dio.post( 208 | path, 209 | data: data, 210 | queryParameters: queryParameters, 211 | options: requestOptions, 212 | cancelToken: cancelToken, 213 | ); 214 | return response.data; 215 | } 216 | 217 | Future put( 218 | String path, { 219 | dynamic data, 220 | Map? queryParameters, 221 | Options? options, 222 | }) async { 223 | Options requestOptions = options ?? Options(); 224 | requestOptions.headers = requestOptions.headers ?? {}; 225 | Map? authorization = getAuthorizationHeader(); 226 | if (authorization != null) { 227 | requestOptions.headers!.addAll(authorization); 228 | } 229 | var response = await dio.put( 230 | path, 231 | data: data, 232 | queryParameters: queryParameters, 233 | options: requestOptions, 234 | cancelToken: cancelToken, 235 | ); 236 | return response.data; 237 | } 238 | 239 | // restful patch 操作 240 | Future patch( 241 | String path, { 242 | dynamic data, 243 | Map? queryParameters, 244 | Options? options, 245 | }) async { 246 | Options requestOptions = options ?? Options(); 247 | requestOptions.headers = requestOptions.headers ?? {}; 248 | Map? authorization = getAuthorizationHeader(); 249 | if (authorization != null) { 250 | requestOptions.headers!.addAll(authorization); 251 | } 252 | var response = await dio.patch( 253 | path, 254 | data: data, 255 | queryParameters: queryParameters, 256 | options: requestOptions, 257 | cancelToken: cancelToken, 258 | ); 259 | return response.data; 260 | } 261 | 262 | // restful delete 操作 263 | Future delete( 264 | String path, { 265 | dynamic data, 266 | Map? queryParameters, 267 | Options? options, 268 | }) async { 269 | Options requestOptions = options ?? Options(); 270 | requestOptions.headers = requestOptions.headers ?? {}; 271 | Map? authorization = getAuthorizationHeader(); 272 | if (authorization != null) { 273 | requestOptions.headers!.addAll(authorization); 274 | } 275 | var response = await dio.delete( 276 | path, 277 | data: data, 278 | queryParameters: queryParameters, 279 | options: requestOptions, 280 | cancelToken: cancelToken, 281 | ); 282 | return response.data; 283 | } 284 | 285 | // restful post form 表单提交操作 286 | Future postForm( 287 | String path, { 288 | dynamic data, 289 | Map? queryParameters, 290 | Options? options, 291 | }) async { 292 | Options requestOptions = options ?? Options(); 293 | requestOptions.headers = requestOptions.headers ?? {}; 294 | Map? authorization = getAuthorizationHeader(); 295 | if (authorization != null) { 296 | requestOptions.headers!.addAll(authorization); 297 | } 298 | var response = await dio.post( 299 | path, 300 | data: FormData.fromMap(data), 301 | queryParameters: queryParameters, 302 | options: requestOptions, 303 | cancelToken: cancelToken, 304 | ); 305 | return response.data; 306 | } 307 | 308 | // restful post Stream 流数据 309 | Future postStream( 310 | String path, { 311 | dynamic data, 312 | int dataLength = 0, 313 | Map? queryParameters, 314 | Options? options, 315 | }) async { 316 | Options requestOptions = options ?? Options(); 317 | requestOptions.headers = requestOptions.headers ?? {}; 318 | Map? authorization = getAuthorizationHeader(); 319 | if (authorization != null) { 320 | requestOptions.headers!.addAll(authorization); 321 | } 322 | requestOptions.headers!.addAll({ 323 | Headers.contentLengthHeader: dataLength.toString(), 324 | }); 325 | var response = await dio.post( 326 | path, 327 | data: Stream.fromIterable(data.map((e) => [e])), 328 | queryParameters: queryParameters, 329 | options: requestOptions, 330 | cancelToken: cancelToken, 331 | ); 332 | return response.data; 333 | } 334 | } 335 | 336 | // 异常处理 337 | class ErrorEntity implements Exception { 338 | int code = -1; 339 | String message = ""; 340 | ErrorEntity({required this.code, required this.message}); 341 | 342 | String toString() { 343 | if (message == "") return "Exception"; 344 | return "Exception: code $code, $message"; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /lib/common/utils/index.dart: -------------------------------------------------------------------------------- 1 | export './storage.dart'; 2 | export './http.dart'; 3 | export './loading.dart'; 4 | export './timex.dart'; 5 | export './platform.dart'; 6 | -------------------------------------------------------------------------------- /lib/common/utils/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | 4 | class Loading { 5 | Loading() { 6 | EasyLoading.instance 7 | ..displayDuration = const Duration(milliseconds: 2000) 8 | ..indicatorType = EasyLoadingIndicatorType.ring 9 | ..loadingStyle = EasyLoadingStyle.custom 10 | ..indicatorSize = 35.0 11 | ..lineWidth = 2 12 | ..radius = 10.0 13 | ..progressColor = Colors.white 14 | ..backgroundColor = Colors.black.withOpacity(0.7) 15 | ..indicatorColor = Colors.white 16 | ..textColor = Colors.white 17 | ..maskColor = Colors.black.withOpacity(0.6) 18 | ..userInteractions = true 19 | ..dismissOnTap = false 20 | ..maskType = EasyLoadingMaskType.custom; 21 | } 22 | 23 | static void show([String? text]) { 24 | EasyLoading.instance.userInteractions = false; 25 | EasyLoading.show(status: text ?? 'Loading...'); 26 | } 27 | 28 | static void toast(String text) { 29 | EasyLoading.showToast(text); 30 | } 31 | 32 | static void dismiss() { 33 | EasyLoading.instance.userInteractions = true; 34 | EasyLoading.dismiss(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/common/utils/platform.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | String getPlatform() { 4 | // "android" "fuchsia" "ios" "linux" "macos" "windows" 5 | return Platform.operatingSystem; 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/utils/storage.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class StorageService extends GetxService { 5 | static StorageService get to => Get.find(); 6 | late final SharedPreferences _prefs; 7 | 8 | Future init() async { 9 | _prefs = await SharedPreferences.getInstance(); 10 | return this; 11 | } 12 | 13 | Future setString(String key, String value) async { 14 | return await _prefs.setString(key, value); 15 | } 16 | 17 | Future setBool(String key, bool value) async { 18 | return await _prefs.setBool(key, value); 19 | } 20 | 21 | Future setList(String key, List value) async { 22 | return await _prefs.setStringList(key, value); 23 | } 24 | 25 | String getString(String key) { 26 | return _prefs.getString(key) ?? ''; 27 | } 28 | 29 | bool getBool(String key) { 30 | return _prefs.getBool(key) ?? false; 31 | } 32 | 33 | List getList(String key) { 34 | return _prefs.getStringList(key) ?? []; 35 | } 36 | 37 | Future remove(String key) async { 38 | return await _prefs.remove(key); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/common/utils/timex.dart: -------------------------------------------------------------------------------- 1 | timeStampToString(int timeStamp) { 2 | var _nowDate = DateTime.now(); //当前日期 3 | var _conDate = 4 | DateTime.fromMillisecondsSinceEpoch(timeStamp); //传入的日期 millTime为毫秒级时间戳 5 | 6 | String _returnTime = ''; //转换后的时间 7 | var flagMonth = 0; 8 | 9 | if (_nowDate.year != _conDate.year) { 10 | _returnTime = '${_conDate.year}年'; 11 | } 12 | 13 | if (_nowDate.month != _conDate.month) { 14 | _returnTime = _returnTime + '${_conDate.month}月'; 15 | flagMonth = 1; 16 | } 17 | 18 | if (_nowDate.day != _conDate.day) { 19 | if (flagMonth == 0) { 20 | _returnTime = _returnTime + '${_conDate.month}月'; 21 | } 22 | _returnTime = _returnTime + '${_conDate.day}日 '; 23 | return _returnTime; 24 | } 25 | 26 | // 凌晨:0时至5时;早晨:5时至8时;上午:8时至11时;中午:11时至13时;下午:13时至16时;傍晚:16时至19时;晚上:19时至24时。 27 | int _conHour = _conDate.hour; 28 | 29 | // if (_conHour >= 0 && _conHour < 5) { 30 | // _returnTime = '凌晨'; 31 | // } else if (_conHour >= 5 && _conHour < 8) { 32 | // _returnTime = '早晨'; 33 | // } else if (_conHour >= 8 && _conHour < 11) { 34 | // _returnTime = '上午'; 35 | // } else if (_conHour >= 11 && _conHour < 13) { 36 | // _returnTime = '中午'; 37 | // } else if (_conHour >= 13 && _conHour < 16) { 38 | // _returnTime = '下午'; 39 | // } else if (_conHour >= 16 && _conHour < 19) { 40 | // _returnTime = '傍晚'; 41 | // } else if (_conHour >= 19 && _conHour <= 23) { 42 | // _returnTime = '晚上'; 43 | // } 44 | 45 | return _returnTime + 46 | _conHour.toString().padLeft(2, '0') + 47 | ':' + 48 | _conDate.minute.toString().padLeft(2, '0'); 49 | } 50 | -------------------------------------------------------------------------------- /lib/common/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:uuid/uuid.dart'; 2 | 3 | String genUuid() { 4 | var uuid = Uuid(); 5 | return uuid.v1(); 6 | } 7 | -------------------------------------------------------------------------------- /lib/common/values/storage.dart: -------------------------------------------------------------------------------- 1 | // 用户 - 配置信息 2 | const String STORAGE_USER_PROFILE_KEY = 'user_profile'; 3 | 4 | // 用户 - 配置信息 5 | const String STORAGE_USER_TOKEN_KEY = 'user_token'; 6 | 7 | // 设备是否第一次打开 8 | const String STORAGE_DEVICE_FIRST_OPEN_KEY = 'device_first_open'; 9 | 10 | // 首页新闻cacheKey 11 | const String STORAGE_INDEX_NEWS_CACHE_KEY = 'cache_index_news'; 12 | 13 | // 多语言 14 | const String STORAGE_LANGUAGE_CODE = 'language_code'; 15 | -------------------------------------------------------------------------------- /lib/common/widgets/app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wechat_flutter/common/style/colors.dart'; 5 | import 'package:wechat_flutter/common/style/icons.dart'; 6 | import 'package:wechat_flutter/pages/more/add_friend/index.dart'; 7 | 8 | enum ActionItems { GROUP_CHAT, ADD_FRIEND, QR_SCAN, PAYMENT, HELP } 9 | 10 | AppBar BuildAppBar(String title) { 11 | return AppBar( 12 | centerTitle: true, 13 | elevation: 0, 14 | toolbarHeight: 36, 15 | shadowColor: Colors.grey.shade300, 16 | backgroundColor: Colors.grey.shade300, 17 | title: Text( 18 | title, 19 | style: TextStyle(color: Colors.black, fontSize: 16), 20 | ), 21 | actions: [ 22 | Container( 23 | padding: EdgeInsets.only(right: 16.0), 24 | child: IconButton( 25 | icon: Icon( 26 | ICons.SEARCH, 27 | color: Color(AppColors.APPBarTextColor), 28 | ), 29 | onPressed: () { 30 | print('点击了搜索按钮'); 31 | }, 32 | ), 33 | ), 34 | SizedBox( 35 | width: 10, 36 | ), 37 | Theme( 38 | data: ThemeData(cardColor: Color(AppColors.APPCardColor)), 39 | child: PopupMenuButton( 40 | icon: Icon( 41 | ICons.ADD, 42 | color: Color(AppColors.APPBarTextColor), 43 | ), 44 | itemBuilder: (BuildContext context) { 45 | return >[ 46 | PopupMenuItem( 47 | child: _buildPopupMenuItem( 48 | Icon( 49 | ICons.GROUP_CHAT, 50 | size: 22.0, 51 | color: Color(AppColors.AppBarPopupMenuTextColor), 52 | ), 53 | '发起群聊'), 54 | value: ActionItems.GROUP_CHAT, 55 | ), 56 | PopupMenuItem( 57 | child: _buildPopupMenuItem( 58 | Icon(ICons.ADD_FRIEND, 59 | size: 22.0, 60 | color: Color(AppColors.AppBarPopupMenuTextColor)), 61 | '添加朋友'), 62 | value: ActionItems.ADD_FRIEND, 63 | ), 64 | PopupMenuItem( 65 | child: _buildPopupMenuItem( 66 | Icon(ICons.QR_SCAN, 67 | size: 22.0, 68 | color: Color(AppColors.AppBarPopupMenuTextColor)), 69 | '扫一扫'), 70 | value: ActionItems.QR_SCAN, 71 | ), 72 | PopupMenuItem( 73 | child: _buildPopupMenuItem( 74 | Icon(ICons.PAYMENT, 75 | size: 22.0, 76 | color: Color(AppColors.AppBarPopupMenuTextColor)), 77 | '收付款'), 78 | value: ActionItems.PAYMENT, 79 | ), 80 | PopupMenuItem( 81 | child: _buildPopupMenuItem( 82 | Icon(ICons.HELP, 83 | size: 22.0, 84 | color: Color(AppColors.AppBarPopupMenuTextColor)), 85 | '帮助与反馈'), 86 | value: ActionItems.HELP, 87 | ) 88 | ]; 89 | }, 90 | onSelected: (ActionItems selected) { 91 | if (selected == ActionItems.GROUP_CHAT) { 92 | print('点击了发起群聊'); 93 | } else if (selected == ActionItems.ADD_FRIEND) { 94 | print('点击了添加朋友'); 95 | Get.to(AddFriendPage(), transition: Transition.rightToLeft); 96 | } else if (selected == ActionItems.QR_SCAN) { 97 | print('点击了扫一扫'); 98 | } else if (selected == ActionItems.PAYMENT) { 99 | print('点击了收付款'); 100 | } else if (selected == ActionItems.HELP) { 101 | print('点击了帮助与反馈'); 102 | } 103 | print(selected); 104 | }, 105 | ), 106 | ), 107 | SizedBox( 108 | width: 10, 109 | ), 110 | ], 111 | systemOverlayStyle: SystemUiOverlayStyle( 112 | statusBarBrightness: Brightness.dark, 113 | statusBarColor: Colors.grey.shade300, 114 | ), 115 | ); 116 | } 117 | 118 | _buildPopupMenuItem(Widget icon, String title) { 119 | return Row( 120 | children: [ 121 | Container( 122 | padding: EdgeInsets.only(right: 12), 123 | child: icon, 124 | ), 125 | Text( 126 | title, 127 | style: TextStyle(color: Color(AppColors.AppBarPopupMenuTextColor)), 128 | ) 129 | ], 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /lib/common/widgets/chat_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | import 'package:wechat_flutter/common/style/colors.dart'; 4 | import 'package:wechat_flutter/common/style/menus.dart'; 5 | 6 | import 'user_avatar.dart'; 7 | 8 | class ChatContentView extends StatelessWidget { 9 | bool isSelf; //false 代表对方 , true代表自己 10 | String text; //聊天内容 11 | String avatar; //头像url 12 | String username; //昵称 13 | int type; //聊天类型 1单聊 2群聊 14 | ChatContentView( 15 | {Key? key, 16 | required this.isSelf, 17 | required this.text, 18 | required this.avatar, 19 | required this.username, 20 | required this.type}) 21 | : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var tapPos; 26 | //头像组件 27 | Widget userAvatar = UserAvatar( 28 | width: 40, 29 | height: 40, 30 | image: avatar, 31 | onPressed: () { 32 | print('点击头像'); 33 | }); 34 | 35 | Widget userNameWidget = Container( 36 | margin: EdgeInsets.only( 37 | left: isSelf == false ? 14 : 0, 38 | bottom: 5, 39 | right: isSelf == false ? 0 : 14), 40 | child: Text( 41 | username, 42 | style: TextStyle( 43 | color: Color(AppColors.ChatTime), 44 | fontSize: 12.0), 45 | ), 46 | ); 47 | 48 | _showMenu(BuildContext context, Offset tapPos) { 49 | RenderBox overlay = context.findRenderObject()! as RenderBox; 50 | final RelativeRect position = RelativeRect.fromLTRB( 51 | tapPos.dx, tapPos.dy, -tapPos.dx, overlay.size.height - tapPos.dy); 52 | showMenu( 53 | context: context, 54 | position: position, 55 | items: >[ 56 | PopupMenuItem( 57 | child: Text(MessageDetailSelects.MENU_COPY_VALUE), 58 | value: MessageDetailSelects.MENU_COPY, 59 | ), 60 | PopupMenuItem( 61 | child: Text(MessageDetailSelects.MENU_SHARE_FRIENDS_VALUE), 62 | value: MessageDetailSelects.MENU_SHARE_FRIENDS, 63 | ), 64 | PopupMenuItem( 65 | child: Text(MessageDetailSelects.MENU_FAVORIITE_VALUE), 66 | value: MessageDetailSelects.MENU_FAVORIITE, 67 | ), 68 | PopupMenuItem( 69 | child: Text(MessageDetailSelects.MENU_REMIND_VALUE), 70 | value: MessageDetailSelects.MENU_REMIND, 71 | ), 72 | PopupMenuItem( 73 | child: Text(MessageDetailSelects.MENU_TRANSLATE_VALUE), 74 | value: MessageDetailSelects.MENU_TRANSLATE, 75 | ), 76 | PopupMenuItem( 77 | child: Text(MessageDetailSelects.MENU_DELATE_VALUE), 78 | value: MessageDetailSelects.MENU_DELATE, 79 | ), 80 | PopupMenuItem( 81 | child: Text(MessageDetailSelects.MENU_MULTIPE_CHOICE_VALUE), 82 | value: MessageDetailSelects.MENU_MULTIPE_CHOICE, 83 | ), 84 | ]).then((String? selected) { 85 | print('当前选中的是:$selected'); 86 | }); 87 | } 88 | 89 | Widget messageTextWidget = InkWell( 90 | onTapDown: (TapDownDetails details) { 91 | tapPos = details.globalPosition; 92 | }, 93 | onLongPress: () { 94 | _showMenu(context, tapPos); 95 | print('弹出会话菜单'); 96 | }, 97 | child: Container( 98 | child: Text( 99 | text, 100 | style: TextStyle( 101 | fontSize: 14.0, 102 | color: Color(AppColors.TextBobule), 103 | height: 1.2), 104 | ), 105 | padding: EdgeInsets.all(10), 106 | decoration: BoxDecoration( 107 | borderRadius: BorderRadius.circular(6.0), 108 | color: isSelf == false 109 | ? Color(AppColors.TextBobuleLeft) 110 | : Color(AppColors.TextBobuleRight), 111 | ), 112 | ), 113 | ); 114 | 115 | final List nameAndText = [userNameWidget, messageTextWidget]; 116 | final List onlyText = [SizedBox(height: 5), messageTextWidget]; 117 | textBubble() { 118 | return Expanded( 119 | child: Column( 120 | crossAxisAlignment: 121 | isSelf == false ? CrossAxisAlignment.start : CrossAxisAlignment.end, 122 | children: type == 2 && isSelf == false ? nameAndText : onlyText, 123 | )); 124 | } 125 | 126 | return Container( 127 | margin: EdgeInsets.only( 128 | bottom: 6, 129 | top: 6), 130 | child: isSelf == false 131 | ? Row( 132 | crossAxisAlignment: CrossAxisAlignment.start, 133 | children: [ 134 | SizedBox(width: 10), 135 | userAvatar, 136 | SizedBox(width: 10), 137 | textBubble(), 138 | SizedBox(width: 50), 139 | ], 140 | ) 141 | : Row( 142 | crossAxisAlignment: CrossAxisAlignment.start, 143 | children: [ 144 | SizedBox(width: 50), 145 | textBubble(), 146 | SizedBox(width: 10), 147 | userAvatar, 148 | SizedBox(width: 10), 149 | ], 150 | ), 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/common/widgets/network_img.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import "package:cached_network_image/cached_network_image.dart"; 4 | 5 | class CacheImg extends StatelessWidget { 6 | final String image; 7 | final double width; 8 | final double height; 9 | final String placeholderImg; 10 | CacheImg( 11 | this.image, { 12 | this.width = 50, 13 | this.height = 50, 14 | this.placeholderImg = "assets/images/icon.png", 15 | Key? key, 16 | }) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ClipRRect( 21 | borderRadius: BorderRadius.all(Radius.circular(5.0)), 22 | child: CachedNetworkImage( 23 | imageUrl: image, 24 | // placeholder: (context, url) => Image.asset(placeholderImg), 25 | placeholder: (context, url) => const CircularProgressIndicator(), 26 | fit: BoxFit.fitWidth, 27 | width: width, 28 | height: height, 29 | )); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/common/widgets/password.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PasswordField extends StatefulWidget { 4 | const PasswordField({ 5 | Key? key, 6 | this.restorationId, 7 | this.fieldKey, 8 | this.hintText, 9 | this.labelText, 10 | this.helperText, 11 | this.onSaved, 12 | this.validator, 13 | this.onFieldSubmitted, 14 | this.focusNode, 15 | this.textInputAction, 16 | }) : super(key: key); 17 | 18 | final String? restorationId; 19 | final Key? fieldKey; 20 | final String? hintText; 21 | final String? labelText; 22 | final String? helperText; 23 | final FormFieldSetter? onSaved; 24 | final FormFieldValidator? validator; 25 | final ValueChanged? onFieldSubmitted; 26 | final FocusNode? focusNode; 27 | final TextInputAction? textInputAction; 28 | 29 | @override 30 | State createState() => _PasswordFieldState(); 31 | } 32 | 33 | class _PasswordFieldState extends State with RestorationMixin { 34 | final RestorableBool _obscureText = RestorableBool(true); 35 | 36 | @override 37 | String? get restorationId => widget.restorationId; 38 | 39 | @override 40 | void restoreState(RestorationBucket? oldBucket, bool initialRestore) { 41 | registerForRestoration(_obscureText, 'obscure_text'); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return TextFormField( 47 | initialValue: "123456", 48 | key: widget.fieldKey, 49 | restorationId: 'password_text_field', 50 | obscureText: _obscureText.value, 51 | maxLength: 12, 52 | onSaved: widget.onSaved, 53 | validator: widget.validator, 54 | onFieldSubmitted: widget.onFieldSubmitted, 55 | autofillHints: [AutofillHints.password], 56 | decoration: InputDecoration( 57 | filled: true, 58 | hintText: widget.hintText, 59 | labelText: widget.labelText, 60 | helperText: widget.helperText, 61 | border: const OutlineInputBorder(), 62 | icon: const Icon(Icons.password), 63 | suffixIcon: IconButton( 64 | onPressed: () { 65 | setState(() { 66 | _obscureText.value = !_obscureText.value; 67 | }); 68 | }, 69 | hoverColor: Colors.transparent, 70 | icon: Icon( 71 | _obscureText.value ? Icons.visibility : Icons.visibility_off, 72 | ), 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/common/widgets/search_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:wechat_flutter/common/style/colors.dart'; 3 | 4 | class SearchBar extends StatelessWidget { 5 | final GestureTapCallback onTap; 6 | final String text; 7 | final bool isBorder; 8 | 9 | SearchBar({ 10 | Key? key, 11 | required this.text, 12 | required this.onTap, 13 | this.isBorder = false, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return InkWell( 19 | child: Container( 20 | decoration: BoxDecoration( 21 | color: Colors.white, 22 | border: isBorder 23 | ? Border( 24 | bottom: BorderSide(color: Colors.grey, width: 0.2), 25 | ) 26 | : null, 27 | ), 28 | padding: EdgeInsets.symmetric(vertical: 15.0), 29 | child: Row( 30 | children: [ 31 | Padding( 32 | padding: EdgeInsets.symmetric(horizontal: 20.0), 33 | child: Icon(Icons.search, color: Color(AppColors.DesTextColor)), 34 | ), 35 | Text( 36 | text, 37 | style: TextStyle(color: Color(AppColors.DesTextColor)), 38 | ) 39 | ], 40 | ), 41 | ), 42 | onTap: onTap, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/common/widgets/user_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'network_img.dart'; 4 | 5 | class UserAvatar extends StatelessWidget { 6 | final String image; 7 | final VoidCallback onPressed; 8 | final double width; 9 | final double height; 10 | 11 | UserAvatar( 12 | {Key? key, 13 | required this.image, 14 | required this.onPressed, 15 | this.width = 30.0, 16 | this.height = 30.0}) 17 | : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return RawMaterialButton( 22 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 23 | constraints: BoxConstraints(minWidth: 0.0, minHeight: 0.0), 24 | child: CacheImg(image, width: width, height: height), 25 | onPressed: onPressed); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/common/xerr/codes.dart: -------------------------------------------------------------------------------- 1 | class Status { 2 | static const int OK = 0; // 成功 3 | 4 | static const int CLIENT_ERROR = 4000; // 客户端自身错误 5 | static const int UNAUTHORIZED = 4001; // 身份认证失败 6 | static const int PARAM_ERROR = 4002; // 缺少必传参数 7 | static const int NO_DATA = 4003; // 没有数据 8 | static const int DATA_EXIST = 4004; // 数据已存在 9 | 10 | static const int SERVER_ERROR = 5000; // 服务端开小差啦,请稍后再试一试 11 | static const int DB_ERROR = 5001; // 数据库操作异常 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/xerr/index.dart: -------------------------------------------------------------------------------- 1 | export './codes.dart'; 2 | export './msgs.dart'; -------------------------------------------------------------------------------- /lib/common/xerr/msgs.dart: -------------------------------------------------------------------------------- 1 | import './codes.dart'; 2 | 3 | var mapCodeMsg = { 4 | Status.OK: "成功", 5 | Status.CLIENT_ERROR: "客户端自身错误", 6 | Status.UNAUTHORIZED: "身份认证失败", 7 | Status.PARAM_ERROR: "缺少必传参数", 8 | Status.NO_DATA: "没有数据", 9 | Status.DATA_EXIST: "数据已存在", 10 | Status.SERVER_ERROR: "服务端开小差啦,请稍后再试一试", 11 | Status.DB_ERROR: "数据库操作异常", 12 | }; 13 | 14 | String? codeToMsg(int code) { 15 | if (!mapCodeMsg.containsKey(code)) { 16 | return mapCodeMsg[Status.SERVER_ERROR]; 17 | } else { 18 | return mapCodeMsg[code]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/common/xresp/xresp.dart: -------------------------------------------------------------------------------- 1 | class RespBody { 2 | int code; 3 | String msg; 4 | dynamic data; 5 | 6 | RespBody({required this.code, required this.msg, this.data}); 7 | factory RespBody.fromJson(Map json) { 8 | var respBody = RespBody( 9 | code: json['code'] as int, 10 | msg: json['msg'] as String, 11 | data: json['data'] as Map?, 12 | ); 13 | return respBody; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/global.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:wechat_flutter/common/store/user.dart'; 4 | import 'package:wechat_flutter/common/utils/index.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | // 全局静态数据 8 | class Global { 9 | // 初始化 10 | static Future init() async { 11 | WidgetsFlutterBinding.ensureInitialized(); 12 | await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 13 | 14 | setSystemUi(); 15 | Loading(); 16 | 17 | await Get.putAsync(() => StorageService().init()); 18 | 19 | Get.put(UserStore()); 20 | } 21 | 22 | static void setSystemUi() { 23 | if (GetPlatform.isAndroid) { 24 | SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle( 25 | statusBarColor: Colors.transparent, 26 | statusBarBrightness: Brightness.light, 27 | statusBarIconBrightness: Brightness.dark, 28 | systemNavigationBarDividerColor: Colors.transparent, 29 | systemNavigationBarColor: Colors.white, 30 | systemNavigationBarIconBrightness: Brightness.dark, 31 | ); 32 | SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 3 | import 'package:get/get.dart'; 4 | import 'common/langs/translation_service.dart'; 5 | import 'common/routes/routes.dart'; 6 | import './global.dart'; 7 | 8 | Future main() async { 9 | await Global.init(); 10 | runApp(const MyApp()); 11 | } 12 | 13 | class MyApp extends StatelessWidget { 14 | const MyApp({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return GetMaterialApp( 19 | debugShowCheckedModeBanner: false, 20 | initialRoute: AppRouter.INITIAL, 21 | getPages: AppRouter.routes, 22 | unknownRoute: AppRouter.unknownRoute, 23 | // 配置 加载中 效果 24 | builder: EasyLoading.init(), 25 | // 下面这三条配置国际化多语言 26 | locale: TranslationService.locale, 27 | fallbackLocale: TranslationService.fallbackLocale, 28 | translations: TranslationService(), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/pages/chat/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class ChatBinding implements Bindings { 4 | @override 5 | void dependencies() { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/pages/chat/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/apis/message.dart'; 4 | import 'package:wechat_flutter/common/biz/custom_class.dart'; 5 | import 'package:wechat_flutter/common/entities/index.dart'; 6 | import 'package:wechat_flutter/common/utils/utils.dart'; 7 | import 'package:wechat_flutter/pages/home/controller.dart'; 8 | 9 | class ChatController extends GetxController { 10 | static ChatController get to => Get.find(); 11 | 12 | var inputController = TextEditingController(); // 输入内容 13 | var inputType = 1; // 输入类型 14 | 15 | var groupId = ""; 16 | late ChatMsg lastMsg; 17 | var aliasName = ""; 18 | var groupMsgList = []; 19 | 20 | setValue( 21 | {inputContent, inputType, groupId, lastMsg, aliasName, groupMsgList}) { 22 | this.inputController.text = inputContent ?? inputController.text; 23 | this.inputType = inputType ?? this.inputType; 24 | this.lastMsg = lastMsg ?? this.lastMsg; 25 | this.groupId = groupId ?? this.groupId; 26 | this.aliasName = aliasName ?? this.aliasName; 27 | this.groupMsgList = groupMsgList ?? this.groupMsgList; 28 | update(); 29 | } 30 | 31 | /// 在 widget 内存中分配后立即调用。 32 | @override 33 | void onInit() { 34 | super.onInit(); 35 | print("chat on init"); 36 | groupId = Get.arguments["groupId"]; 37 | lastMsg = Get.arguments["lastMsg"]; 38 | aliasName = Get.arguments["aliasName"]; 39 | // 有数据则不用拉取离线消息, 直接返回 40 | if (groupMsgList.isNotEmpty) { 41 | return; 42 | } 43 | // 把最新消息先放入到群组消息列表中 44 | groupMsgList.add(lastMsg); 45 | update(); 46 | // 判断当前maxMsgId的值根据maxMsgId从后往前拉取信息 47 | var maxMsgId = lastMsg.id; 48 | var data = PullRequest( 49 | platform: HomeController.to.platform, 50 | groupId: groupId, 51 | maxMsgId: maxMsgId); 52 | MessageAPI.pull(data).then((pullResp) { 53 | if (pullResp.list.isEmpty) { 54 | print("没有拉取到消息"); 55 | return; 56 | } 57 | int startPos = -1; 58 | for (var msg in pullResp.list) { 59 | var nextIdx = groupMsgList.append(msg, startPos); 60 | startPos = nextIdx; 61 | } 62 | update(); 63 | print("获取群组$groupId的消息列表成功"); 64 | }).catchError((err) { 65 | // 显示弹窗 66 | Get.snackbar("获取消息页数据失败", "$err"); 67 | }); 68 | } 69 | 70 | void sendMsg() { 71 | if (inputController.text == "") { 72 | Get.snackbar("提示", "不能发送空白信息"); 73 | } 74 | print("send ${inputController.text}"); 75 | var uuid = genUuid(); 76 | var data = UploadRequest( 77 | groupId: groupId, content: inputController.text, type: 1, uuid: uuid); 78 | MessageAPI.upload(data).then((uploadResp) { 79 | print("消息上传成功"); 80 | this.inputController.text = ""; // 清空输入栏 81 | update(); 82 | }).catchError((err) { 83 | // 显示弹窗 84 | Get.snackbar("消息上传失败", "$err"); 85 | }); 86 | } 87 | 88 | /// 在 onInit() 之后调用 1 帧。这是进入的理想场所 89 | @override 90 | void onReady() { 91 | super.onReady(); 92 | } 93 | 94 | /// 在 [onDelete] 方法之前调用。 95 | @override 96 | void onClose() { 97 | super.onClose(); 98 | } 99 | 100 | /// dispose 释放内存 101 | @override 102 | void dispose() { 103 | super.dispose(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/pages/chat/index.dart: -------------------------------------------------------------------------------- 1 | library chat; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/chat/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/style/icons.dart'; 4 | import 'package:wechat_flutter/common/widgets/app_bar.dart'; 5 | import 'package:wechat_flutter/common/widgets/chat_content.dart'; 6 | import 'package:wechat_flutter/pages/message/controller.dart'; 7 | import 'package:wechat_flutter/pages/mine/controller.dart'; 8 | 9 | import 'controller.dart'; 10 | 11 | class ChatPage extends StatelessWidget { 12 | final String tag; 13 | final ChatController controller; 14 | ChatPage({Key? key, required this.tag}) 15 | : controller = Get.put(ChatController(), tag: tag, permanent: true), 16 | super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | backgroundColor: Colors.grey.shade200, 22 | appBar: BuildAppBar(controller.aliasName), 23 | body: GestureDetector( 24 | // behavior: HitTestBehavior.translucent, 25 | child: SafeArea( 26 | child: Column( 27 | children: [ 28 | chatView(), 29 | inputView(), 30 | ], 31 | ), 32 | ), 33 | ), 34 | ); 35 | } 36 | 37 | // 聊天内容显示 38 | Widget chatView() { 39 | return Expanded( 40 | child: GetBuilder( 41 | tag: tag, 42 | builder: (controller) => ListView.builder( 43 | padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2), 44 | itemCount: controller.groupMsgList.length, 45 | itemBuilder: (context, index) { 46 | final chatMsg = controller.groupMsgList[index]; 47 | var senderId = chatMsg.senderId; 48 | var groupId = chatMsg.groupId; 49 | var groupInfo = 50 | MessageController.to.messageGroupInfoMap[groupId]!; 51 | bool isSelf; 52 | String avatar; 53 | String userName; 54 | if (senderId == MineController.to.userId) { 55 | isSelf = true; 56 | avatar = MineController.to.avatarUrl; 57 | userName = MineController.to.nickName; 58 | } else { 59 | isSelf = false; 60 | avatar = groupInfo.avatarUrl; 61 | userName = groupInfo.aliasName; 62 | } 63 | return ChatContentView( 64 | isSelf: isSelf, 65 | text: chatMsg.content, 66 | avatar: avatar, 67 | username: userName, 68 | type: chatMsg.type, 69 | ); 70 | }))); 71 | } 72 | 73 | // 最下方的输入框部分 74 | Widget inputView() { 75 | return Container( 76 | padding: EdgeInsets.only(top: 2.0, bottom: 2.0, left: 5.0, right: 5.0), 77 | color: Color(0xFFF7F7F7), 78 | child: Row( 79 | children: [ 80 | Container( 81 | width: 40.0, 82 | margin: EdgeInsets.only(right: 10.0), 83 | child: IconButton( 84 | icon: Icon(ICons.VOICE), 85 | onPressed: () { 86 | print('切换到语音'); 87 | }), 88 | ), 89 | Expanded( 90 | child: Container( 91 | padding: EdgeInsets.only(top: 10, bottom: 10), 92 | height: 40.0, 93 | decoration: BoxDecoration( 94 | borderRadius: BorderRadius.all(Radius.circular(5.0)), 95 | color: Colors.white), 96 | child: GetBuilder( 97 | tag: tag, 98 | builder: (controller) => TextField( 99 | controller: controller.inputController, 100 | decoration: InputDecoration.collapsed(hintText: null), 101 | maxLines: 1, 102 | autocorrect: true, 103 | autofocus: false, 104 | style: TextStyle(fontSize: 20, color: Color(0xFF333333)), 105 | textAlign: TextAlign.start, 106 | cursorColor: Colors.green, 107 | onSubmitted: (text) { 108 | print("onSubmitted"); 109 | })), 110 | )), 111 | SizedBox( 112 | width: 40.0, 113 | child: IconButton( 114 | icon: Icon(ICons.FACES), //发送按钮图标 115 | onPressed: () { 116 | print('打开表情面板'); 117 | }), 118 | ), 119 | SizedBox( 120 | width: 40.0, 121 | child: GetBuilder( 122 | tag: tag, 123 | builder: (controller) => IconButton( 124 | //发送按钮或者+按钮 125 | icon: Icon(Icons.send), //发送按钮图标 126 | onPressed: controller.sendMsg)), 127 | ) 128 | ], 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/pages/discover/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class DiscoverBinding implements Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => DiscoverController(), fenix: true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/discover/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class DiscoverController extends GetxController { 6 | DiscoverController(); 7 | 8 | // 在 widget 内存中分配后立即调用。 9 | @override 10 | void onInit() { 11 | super.onInit(); 12 | } 13 | 14 | // 在 onInit() 之后调用 1 帧。这是进入的理想场所 15 | @override 16 | void onReady() { 17 | super.onReady(); 18 | } 19 | 20 | // 在 [onDelete] 方法之前调用。 21 | @override 22 | void onClose() { 23 | super.onClose(); 24 | } 25 | 26 | // dispose 释放内存 27 | @override 28 | void dispose() { 29 | super.dispose(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/pages/discover/index.dart: -------------------------------------------------------------------------------- 1 | library discover; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/discover/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/store/user.dart'; 4 | import 'package:wechat_flutter/common/widgets/app_bar.dart'; 5 | import 'package:wechat_flutter/pages/friend/index.dart'; 6 | import 'package:wechat_flutter/pages/message/controller.dart'; 7 | import 'package:wechat_flutter/pages/mine/index.dart'; 8 | 9 | import 'controller.dart'; 10 | 11 | class DiscoverPage extends StatelessWidget { 12 | const DiscoverPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | backgroundColor: Colors.grey.shade200, 18 | appBar: BuildAppBar("发 现"), 19 | body: SingleChildScrollView( 20 | child: Column( 21 | children: [ 22 | buildItem(() {}, "assets/images/wechat_moment.png", "朋友圈"), 23 | SizedBox( 24 | height: 10, 25 | ), 26 | buildItem(() {}, "assets/images/video.png", "视频号"), 27 | SizedBox( 28 | height: 10, 29 | ), 30 | buildItem(() {}, "assets/images/scan.png", "扫一扫"), 31 | buildItem(() {}, "assets/images/shake.png", "摇一摇"), 32 | SizedBox( 33 | height: 10, 34 | ), 35 | buildItem(() {}, "assets/images/view.png", "看一看"), 36 | buildItem(() {}, "assets/images/searchs.png", "搜一搜"), 37 | SizedBox( 38 | height: 10, 39 | ), 40 | buildItem(() {}, "assets/images/miniprogram.png", "小程序"), 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | Ink buildItem(Function press, String image, String text) { 49 | return Ink( 50 | child: InkWell( 51 | child: Container( 52 | color: Colors.white, 53 | child: Row( 54 | children: [ 55 | SizedBox( 56 | width: 15, 57 | ), 58 | Image.asset( 59 | image, 60 | width: 20, 61 | ), 62 | SizedBox( 63 | width: 12, 64 | ), 65 | Expanded( 66 | child: Container( 67 | padding: EdgeInsets.only(top: 15, right: 15, bottom: 15), 68 | decoration: BoxDecoration( 69 | border: Border( 70 | bottom: 71 | BorderSide(color: Colors.grey.shade200, width: 1))), 72 | child: Row( 73 | children: [ 74 | Expanded( 75 | child: Text( 76 | text, 77 | style: TextStyle(color: Colors.black, fontSize: 15), 78 | )), 79 | Image.asset( 80 | "assets/images/next.png", 81 | width: 16, 82 | color: Colors.grey, 83 | ) 84 | ], 85 | ), 86 | )) 87 | ], 88 | ), 89 | ), 90 | onTap: () { 91 | if (text == "退出") { 92 | print("退出"); 93 | UserStore.to.rmToken(); 94 | Get.delete(); 95 | Get.delete(); 96 | Get.delete(); 97 | Get.delete(); 98 | Get.offAllNamed("/login"); 99 | } 100 | }, 101 | ), 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /lib/pages/frame/login/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class LoginBinding implements Bindings { 4 | @override 5 | void dependencies() { 6 | // Get.lazyPut(() => LoginController()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/pages/frame/login/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/apis/user.dart'; 4 | import 'package:wechat_flutter/common/entities/index.dart'; 5 | import 'package:wechat_flutter/common/routes/routes.dart'; 6 | import 'package:wechat_flutter/common/store/user.dart'; 7 | 8 | class LoginController extends GetxController { 9 | static LoginController get to => Get.find(); 10 | 11 | final formKey = GlobalKey(); // 校验结果 12 | String userEmail = ''; 13 | String userPassword = ''; 14 | 15 | String? emailValidator(String? value) { 16 | if (value!.isEmpty || !value.contains('@')) { 17 | return 'Please enter a valid email address.'; 18 | } 19 | return null; 20 | } 21 | 22 | String? passwordValidator(String? value) { 23 | if (value!.isEmpty || value.length < 6) { 24 | return 'Password must be at least 6 characters long.'; 25 | } 26 | return null; 27 | } 28 | 29 | // 提交 登录 请求 30 | void submitLogin() { 31 | final isValid = formKey.currentState!.validate(); 32 | print("isValid:$isValid"); 33 | Get.focusScope!.unfocus(); 34 | 35 | if (isValid) { 36 | formKey.currentState!.save(); 37 | // 发送 登录请求 38 | var data = LoginRequest(email: userEmail, password: userPassword); 39 | UserAPI.login(data: data).then((loginResp) { 40 | // 写入token到系统中 41 | UserStore.to.setToken(loginResp.accessToken); 42 | // 跳转首页 43 | Get.offAllNamed(AppRouter.Home); 44 | }).catchError((respBody) { 45 | // 显示弹窗 46 | Get.snackbar("login_err_title".tr, "${respBody.code} ${respBody.msg}"); 47 | }); 48 | } 49 | } 50 | 51 | // 提交注册请求 52 | void submitRegister() { 53 | final isValid = formKey.currentState!.validate(); 54 | print("isValid:$isValid"); 55 | Get.focusScope!.unfocus(); 56 | 57 | if (isValid) { 58 | formKey.currentState!.save(); 59 | // 发送注册请求 60 | var data = RegisterRequest(email: userEmail, password: userPassword); 61 | UserAPI.register(data: data).then((registerResp) { 62 | print("注册成功"); 63 | // 显示弹窗 64 | Get.snackbar("注册成功", ""); 65 | }).catchError((respBody) { 66 | // 显示弹窗 67 | Get.snackbar( 68 | "register_err_title".tr, "${respBody.code} ${respBody.msg}"); 69 | }); 70 | } 71 | } 72 | 73 | // 在 widget 内存中分配后立即调用。 74 | @override 75 | void onInit() { 76 | super.onInit(); 77 | } 78 | 79 | // 在 onInit() 之后调用 1 帧。这是进入的理想场所 80 | @override 81 | void onReady() { 82 | super.onReady(); 83 | } 84 | 85 | // 在 [onDelete] 方法之前调用。 86 | @override 87 | void onClose() { 88 | super.onClose(); 89 | } 90 | 91 | // dispose 释放内存 92 | @override 93 | void dispose() { 94 | super.dispose(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/pages/frame/login/index.dart: -------------------------------------------------------------------------------- 1 | library login; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/frame/login/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wechat_flutter/common/widgets/password.dart'; 5 | 6 | import 'index.dart'; 7 | 8 | class LoginPage extends StatelessWidget { 9 | LoginPage({Key? key}) : super(key: key); 10 | final controller = Get.put(LoginController()); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GetBuilder( 15 | builder: (_) { 16 | return Scaffold( 17 | body: Container( 18 | padding: EdgeInsets.symmetric(horizontal: 20.0), 19 | child: Center( 20 | child: Column( 21 | mainAxisSize: MainAxisSize.min, 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | Row( 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | children: [ 27 | Text("login_title".tr, style: TextStyle(fontSize: 25)), 28 | ], 29 | ), 30 | SizedBox(height: 30), 31 | _buildForm(), 32 | SizedBox(height: 20), 33 | // 注册 与 登录按钮 34 | _buildButton(), 35 | ], 36 | )))); 37 | }, 38 | ); 39 | } 40 | 41 | Widget _buildForm() { 42 | return GetBuilder( 43 | builder: (controller) => Form( 44 | key: LoginController.to.formKey, 45 | autovalidateMode: AutovalidateMode.onUserInteraction, 46 | child: AutofillGroup( 47 | child: Column( 48 | children: [ 49 | TextFormField( 50 | restorationId: 'email_field', 51 | initialValue: "ws156858@163.com", 52 | autofocus: true, 53 | autofillHints: [AutofillHints.email], 54 | textInputAction: TextInputAction.next, 55 | validator: LoginController.to.emailValidator, 56 | decoration: InputDecoration( 57 | filled: true, 58 | border: const OutlineInputBorder(), 59 | icon: const Icon(Icons.email), 60 | hintText: "login_email_hint".tr, 61 | labelText: "login_email".tr, 62 | ), 63 | keyboardType: TextInputType.emailAddress, 64 | onSaved: (value) { 65 | LoginController.to.userEmail = value!; 66 | }, 67 | onEditingComplete: () => TextInput.finishAutofillContext(), 68 | ), 69 | SizedBox(height: 25), 70 | PasswordField( 71 | labelText: "login_password".tr, 72 | hintText: "login_password_hint".tr, 73 | validator: LoginController.to.passwordValidator, 74 | textInputAction: TextInputAction.next, 75 | onSaved: (value) { 76 | LoginController.to.userPassword = value!; 77 | }, 78 | ), 79 | ], 80 | )))); 81 | } 82 | 83 | Widget _buildButton() { 84 | return Row( 85 | mainAxisAlignment: MainAxisAlignment.center, 86 | children: [ 87 | ElevatedButton( 88 | child: Padding( 89 | padding: const EdgeInsets.all(10), 90 | child: Text("login_button_register".tr), 91 | ), 92 | onPressed: () { 93 | LoginController.to.submitRegister(); 94 | }, 95 | ), 96 | SizedBox(width: 20), 97 | ElevatedButton( 98 | child: Padding( 99 | padding: const EdgeInsets.all(10), 100 | child: Text("login_button_login".tr), 101 | ), 102 | onPressed: () { 103 | LoginController.to.submitLogin(); 104 | }, 105 | ) 106 | ], 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/pages/frame/notfound/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class NotFoundBinding implements Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => NotFoundController()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/frame/notfound/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class NotFoundController extends GetxController { 4 | NotFoundController(); 5 | 6 | _initData() { 7 | update(["register"]); 8 | } 9 | 10 | void onTap() {} 11 | 12 | // @override 13 | // void onInit() { 14 | // super.onInit(); 15 | // } 16 | 17 | @override 18 | void onReady() { 19 | super.onReady(); 20 | _initData(); 21 | } 22 | 23 | // @override 24 | // void onClose() { 25 | // super.onClose(); 26 | // } 27 | 28 | // @override 29 | // void dispose() { 30 | // super.dispose(); 31 | // } 32 | } 33 | -------------------------------------------------------------------------------- /lib/pages/frame/notfound/index.dart: -------------------------------------------------------------------------------- 1 | export './controller.dart'; 2 | export './view.dart'; 3 | export './bindings.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/frame/notfound/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | 5 | class NotFoundView extends StatelessWidget { 6 | const NotFoundView({Key? key}) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar(title: Text("路由没有找到")), 12 | body: ListTile( 13 | title: Text("返回首页"), 14 | onTap: () => Get.offAllNamed('/home'), 15 | )); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/pages/friend/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class FriendBinding implements Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => FriendController(), fenix: true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/friend/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'index.dart'; 4 | 5 | class FriendController extends GetxController { 6 | static FriendController get to => Get.find(); 7 | 8 | /// 在 widget 内存中分配后立即调用。 9 | @override 10 | void onInit() { 11 | super.onInit(); 12 | } 13 | 14 | /// 在 onInit() 之后调用 1 帧。这是进入的理想场所 15 | @override 16 | void onReady() { 17 | super.onReady(); 18 | } 19 | 20 | /// 在 [onDelete] 方法之前调用。 21 | @override 22 | void onClose() { 23 | super.onClose(); 24 | } 25 | 26 | /// dispose 释放内存 27 | @override 28 | void dispose() { 29 | super.dispose(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/pages/friend/index.dart: -------------------------------------------------------------------------------- 1 | library friend; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/friend/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | import 'package:wechat_flutter/common/mock/data.dart'; 4 | import 'package:wechat_flutter/common/widgets/app_bar.dart'; 5 | 6 | class FriendPage extends StatelessWidget { 7 | const FriendPage({Key? key}) : super(key: key); 8 | 9 | final String labels = "↑ABCDEFGHIJKLMNOPQRSTUVWSYZ#"; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Scaffold( 14 | appBar: BuildAppBar("好 友"), 15 | body: Stack( 16 | alignment: Alignment.centerRight, 17 | children: [ 18 | ListView.builder( 19 | itemCount: Data.contact.keys.length, 20 | itemBuilder: (context, index) { 21 | String key = Data.contact.keys.toList()[index]; 22 | List pItem = Data.contact[key]; 23 | return key == "first" 24 | ? buildContactSysButton() 25 | : buildContactors(context, index, pItem); 26 | }), 27 | buildContactRightBar(), 28 | ], 29 | )); 30 | } 31 | 32 | // 右侧字母 33 | Container buildContactRightBar() { 34 | final width = ScreenUtil().setWidth(18); 35 | final height = ScreenUtil().setHeight(18); 36 | return Container( 37 | color: Colors.transparent, 38 | height: labels.length * height, 39 | width: width, 40 | child: ListView.builder( 41 | itemCount: labels.length, 42 | itemBuilder: (context, index) { 43 | return Ink( 44 | child: InkWell( 45 | highlightColor: Colors.green, 46 | splashColor: Colors.green, 47 | hoverColor: Colors.green, 48 | focusColor: Colors.green, 49 | borderRadius: BorderRadius.all(Radius.circular(9)), 50 | onTap: () { 51 | print(labels[index]); 52 | }, 53 | child: Container( 54 | alignment: Alignment.center, 55 | height: height, 56 | width: width, 57 | child: Text( 58 | labels[index], 59 | textAlign: TextAlign.center, 60 | style: TextStyle( 61 | color: Colors.grey, fontSize: 12), 62 | ), 63 | ), 64 | ), 65 | ); 66 | }), 67 | ); 68 | } 69 | 70 | // 联系人列表 71 | Container buildContactors( 72 | BuildContext context, int index, List pItem) { 73 | return Container( 74 | child: Column( 75 | children: [ 76 | // 上面的字母 77 | Container( 78 | padding: EdgeInsets.only(left: 17), 79 | alignment: Alignment.centerLeft, 80 | height: 30, 81 | width: MediaQuery.of(context).size.width, 82 | color: Colors.grey.shade300, 83 | child: Text( 84 | Data.contact.keys.toList()[index], 85 | style: TextStyle(fontSize: 16, color: Colors.grey), 86 | ), 87 | ), 88 | // 属于这个字母的所有联系人 89 | Container( 90 | height: pItem.length * 50.0, 91 | child: ListView.builder( 92 | physics: NeverScrollableScrollPhysics(), 93 | itemCount: pItem.length, 94 | itemBuilder: (context, index) { 95 | Map item = pItem[index]; 96 | return Container( 97 | padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), 98 | child: Row( 99 | children: [ 100 | ClipRRect( 101 | borderRadius: BorderRadius.all(Radius.circular(5)), 102 | child: Image.asset( 103 | item["avator"], 104 | width: 40, 105 | ), 106 | ), 107 | Expanded( 108 | child: Container( 109 | margin: EdgeInsets.only(left: 10), 110 | alignment: Alignment.centerLeft, 111 | height: 40, 112 | decoration: BoxDecoration( 113 | border: Border( 114 | bottom: BorderSide( 115 | width: 0.5, 116 | color: Colors.grey.shade300, 117 | ))), 118 | child: Text( 119 | item["nickName"], 120 | style: TextStyle( 121 | color: Colors.black, 122 | fontSize: 13, 123 | ), 124 | ), 125 | )) 126 | ], 127 | ), 128 | ); 129 | }), 130 | ) 131 | ], 132 | ), 133 | ); 134 | } 135 | 136 | // 联系人系统按钮 137 | Container buildContactSysButton() { 138 | return Container( 139 | height: 4 * 50.0, 140 | child: Column( 141 | children: [ 142 | buildMenuItem("assets/images/addnew.png", "新的朋友", Colors.orange), 143 | buildMenuItem("assets/images/group.png", "群聊", Colors.green), 144 | buildMenuItem("assets/images/label.png", "标签", Colors.blue), 145 | buildMenuItem("assets/images/public.png", "公众号", Colors.blue), 146 | ], 147 | ), 148 | ); 149 | } 150 | 151 | // 联系人列表项 152 | Container buildMenuItem(String avator, String text, Color bgColor) { 153 | return Container( 154 | padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), 155 | child: Row( 156 | children: [ 157 | Container( 158 | width: 40, 159 | padding: EdgeInsets.all(12), 160 | decoration: BoxDecoration( 161 | color: bgColor, 162 | borderRadius: BorderRadius.circular(5), 163 | ), 164 | child: Image.asset( 165 | "$avator", 166 | ), 167 | ), 168 | Expanded( 169 | child: Container( 170 | margin: EdgeInsets.only(left: 10), 171 | alignment: Alignment.centerLeft, 172 | height: 40, 173 | decoration: BoxDecoration( 174 | border: Border( 175 | bottom: BorderSide( 176 | width: 0.5, color: Colors.grey.shade300))), 177 | child: Text( 178 | text, 179 | style: TextStyle( 180 | color: Colors.black, 181 | fontSize: 13, 182 | fontWeight: FontWeight.bold, 183 | ), 184 | ), 185 | ), 186 | ), 187 | ], 188 | )); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/pages/home/bindings.dart: -------------------------------------------------------------------------------- 1 | import '../message/controller.dart'; 2 | import '../friend/controller.dart'; 3 | import '../discover/controller.dart'; 4 | import '../mine/controller.dart'; 5 | import './controller.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | // 这里 把Home需要用到的所有状态都put进去 9 | class HomeBinding extends Bindings { 10 | @override 11 | void dependencies() { 12 | Get.lazyPut(() => MessageController()); 13 | Get.lazyPut(() => FriendController()); 14 | Get.lazyPut(() => DiscoverController()); 15 | Get.lazyPut(() => MineController()); 16 | Get.lazyPut(() => HomeController()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/pages/home/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:wechat_flutter/common/utils/platform.dart'; 3 | 4 | class HomeController extends GetxController { 5 | static HomeController get to => Get.find(); 6 | 7 | final platform = getPlatform(); 8 | var tabIndex = 0; 9 | 10 | setValue({tabIndex}) { 11 | this.tabIndex = tabIndex ?? this.tabIndex; 12 | update(); 13 | } 14 | 15 | @override 16 | void onInit() { 17 | super.onInit(); 18 | print("home on init"); 19 | } 20 | 21 | @override 22 | void onReady() { 23 | super.onReady(); 24 | print("on ready"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/pages/home/index.dart: -------------------------------------------------------------------------------- 1 | export './controller.dart'; 2 | export './view.dart'; 3 | export './bindings.dart'; 4 | -------------------------------------------------------------------------------- /lib/pages/home/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_screenutil/flutter_screenutil.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import 'controller.dart'; 6 | import '../discover/index.dart'; 7 | import '../friend/index.dart'; 8 | import '../message/index.dart'; 9 | import '../mine/index.dart'; 10 | 11 | class HomePage extends StatelessWidget { 12 | HomePage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | ScreenUtil.init(context); 17 | return Scaffold( 18 | bottomNavigationBar: buildBottomNavigationBar(), 19 | body: buildIndexedStack(), 20 | ); 21 | } 22 | 23 | Widget buildIndexedStack() { 24 | return GetBuilder( 25 | builder: (_) => IndexedStack( 26 | index: HomeController.to.tabIndex, 27 | children: [ 28 | MessagePage(), 29 | FriendPage(), 30 | DiscoverPage(), 31 | MinePage(), 32 | ], 33 | )); 34 | } 35 | 36 | Widget buildBottomNavigationBar() { 37 | return GetBuilder( 38 | builder: (_) => BottomNavigationBar( 39 | currentIndex: HomeController.to.tabIndex, 40 | selectedItemColor: Colors.green, 41 | unselectedItemColor: Colors.grey, 42 | showUnselectedLabels: true, 43 | type: BottomNavigationBarType.fixed, 44 | onTap: (index) { 45 | print(index); 46 | HomeController.to.setValue(tabIndex: index); 47 | }, 48 | items: [ 49 | BottomNavigationBarItem( 50 | label: "消息", 51 | icon: Image.asset( 52 | "assets/images/message.png", 53 | width: 22, 54 | ), 55 | activeIcon: Image.asset( 56 | "assets/images/message.png", 57 | width: 22, 58 | color: Colors.green, 59 | ), 60 | tooltip: "", 61 | ), 62 | BottomNavigationBarItem( 63 | label: "好友", 64 | icon: Image.asset( 65 | "assets/images/contact.png", 66 | width: 22, 67 | ), 68 | activeIcon: Image.asset( 69 | "assets/images/contact.png", 70 | width: 22, 71 | color: Colors.green, 72 | ), 73 | tooltip: "", 74 | ), 75 | BottomNavigationBarItem( 76 | label: "发现", 77 | icon: Image.asset( 78 | "assets/images/find.png", 79 | width: 22, 80 | ), 81 | activeIcon: Image.asset( 82 | "assets/images/find.png", 83 | width: 22, 84 | color: Colors.green, 85 | ), 86 | tooltip: "", 87 | ), 88 | BottomNavigationBarItem( 89 | label: "我的", 90 | icon: Image.asset( 91 | "assets/images/mine.png", 92 | width: 22, 93 | ), 94 | activeIcon: Image.asset( 95 | "assets/images/mine.png", 96 | width: 22, 97 | color: Colors.green, 98 | ), 99 | tooltip: "", 100 | ) 101 | ], 102 | )); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/pages/message/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class MessageBinding implements Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => MessageController(), fenix: true); // 常驻内存 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/message/controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:get/get.dart'; 4 | import 'package:wechat_flutter/common/apis/group.dart'; 5 | import 'package:wechat_flutter/common/biz/custom_class.dart'; 6 | import 'package:wechat_flutter/common/biz/websocket.dart'; 7 | import 'package:wechat_flutter/common/entities/index.dart'; 8 | import 'package:wechat_flutter/common/store/user.dart'; 9 | import 'package:wechat_flutter/pages/chat/index.dart'; 10 | import 'package:wechat_flutter/pages/home/controller.dart'; 11 | 12 | class MessageController extends GetxController { 13 | static MessageController get to => Get.find(); 14 | 15 | late WsSocket wsConn; 16 | var messageGroupInfoMap = {}; 17 | var messageGroupList = []; 18 | 19 | setValue({messageGroupInfoMap, messageGroupList}) { 20 | this.messageGroupInfoMap = messageGroupInfoMap ?? this.messageGroupInfoMap; 21 | this.messageGroupList = messageGroupList ?? this.messageGroupList; 22 | update(); 23 | } 24 | 25 | /// 在 widget 内存中分配后立即调用。 26 | @override 27 | void onInit() { 28 | super.onInit(); 29 | print("message on init"); 30 | // 发送请求 获取消息页数据 31 | var data = MessageGroupInfoListRequest(); 32 | GroupAPI.messageGroupInfoList(data).then((messageGroupInfoListResp) { 33 | var messageGroupInfoList = messageGroupInfoListResp.list; 34 | var messageGroupInfoMap = {}; 35 | var messageGroupList = []; 36 | for (var i = 0; i < messageGroupInfoList.length; i++) { 37 | var groupMsg = messageGroupInfoList[i]; 38 | var groupId = groupMsg.groupId; 39 | messageGroupInfoMap[groupId] = GroupInfo( 40 | groupId: groupId, 41 | avatarUrl: groupMsg.avatarUrl, 42 | aliasName: groupMsg.aliasName); 43 | messageGroupList.add(groupMsg.lastMsg); 44 | } 45 | setValue( 46 | messageGroupInfoMap: messageGroupInfoMap, 47 | messageGroupList: messageGroupList); 48 | print("获取消息页数据成功"); 49 | }).catchError((err) { 50 | // 显示弹窗 51 | Get.snackbar("获取消息页数据失败", "$err"); 52 | }); 53 | // 建立websocket连接, 设置回调 54 | wsConn = WsSocket( 55 | headers: { 56 | "Authorization": UserStore.to.token, 57 | "platform": HomeController.to.platform, 58 | }, 59 | onListen: (message) { 60 | print("收到消息:$message"); 61 | var chatMsg = ChatMsg.fromJson(jsonDecode(message)); 62 | // 根据消息的groupId, 发到对应的ChatController的groupMsgList中 63 | var controller = Get.find(tag: chatMsg.groupId); 64 | controller.groupMsgList.append(chatMsg); 65 | controller.update(); 66 | }, 67 | onError: (error) { 68 | print("收到错误:$error"); 69 | }); 70 | // 开始监听 71 | wsConn.start(); 72 | } 73 | 74 | /// 在 onInit() 之后调用 1 帧。这是进入的理想场所 75 | @override 76 | void onReady() { 77 | super.onReady(); 78 | } 79 | 80 | /// 在 [onDelete] 方法之前调用。 81 | @override 82 | void onClose() { 83 | super.onClose(); 84 | print("退出时关闭websocket连接"); 85 | wsConn.close(); 86 | } 87 | 88 | /// dispose 释放内存 89 | @override 90 | void dispose() { 91 | super.dispose(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/pages/message/index.dart: -------------------------------------------------------------------------------- 1 | library message; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/message/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:wechat_flutter/common/entities/message.dart'; 4 | import 'package:wechat_flutter/common/utils/timex.dart'; 5 | import 'package:wechat_flutter/common/widgets/app_bar.dart'; 6 | import 'package:wechat_flutter/common/widgets/network_img.dart'; 7 | 8 | import '../chat/view.dart'; 9 | import 'controller.dart'; 10 | 11 | class MessagePage extends StatelessWidget { 12 | const MessagePage({Key? key}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: BuildAppBar("消 息"), 18 | body: SizedBox( 19 | child: GetBuilder( 20 | builder: (controller) => ListView.builder( 21 | itemCount: controller.messageGroupList.length, 22 | itemBuilder: (context, index) { 23 | ChatMsg chatMsg = controller.messageGroupList[index]; 24 | var groupId = chatMsg.groupId; 25 | var groupInfo = controller.messageGroupInfoMap[groupId]!; 26 | var time_format = timeStampToString(chatMsg.createTime); 27 | return buildMessageItem(groupId, chatMsg, groupInfo.avatarUrl, 28 | groupInfo.aliasName, time_format); 29 | })), 30 | ), 31 | ); 32 | } 33 | 34 | Container buildMessageItem(String groupId, ChatMsg lastMsg, String avator, 35 | String aliasName, String timeFormat) { 36 | return Container( 37 | padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), 38 | child: InkWell( 39 | onTap: () { 40 | Get.to(() => ChatPage(tag: groupId), 41 | arguments: { 42 | "groupId": groupId, 43 | "lastMsg": lastMsg, 44 | "aliasName": aliasName 45 | }, 46 | transition: Transition.rightToLeft); 47 | }, 48 | child: Row(children: [ 49 | buildMessageItemAvatar(avator), 50 | SizedBox(width: 10), 51 | buildMessageItemContent(aliasName, timeFormat, lastMsg.content) 52 | ])), 53 | ); 54 | } 55 | 56 | ClipRRect buildMessageItemAvatar(String avatarUrl) { 57 | return ClipRRect( 58 | borderRadius: BorderRadius.all(Radius.circular(5)), 59 | child: CacheImg(avatarUrl, width: 40, height: 40), 60 | ); 61 | } 62 | 63 | Expanded buildMessageItemContent( 64 | String name, String time_format, String lastMessage) { 65 | return Expanded( 66 | child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 67 | SizedBox( 68 | height: 4, 69 | ), 70 | Row( 71 | children: [ 72 | Text(name, 73 | style: TextStyle( 74 | color: Colors.black, 75 | fontSize: 14, 76 | )), 77 | Spacer(), 78 | Text(time_format, style: TextStyle(color: Colors.grey, fontSize: 12)), 79 | ], 80 | ), 81 | SizedBox( 82 | height: 5, 83 | ), 84 | Text(lastMessage, style: TextStyle(color: Colors.grey, fontSize: 13)), 85 | SizedBox( 86 | height: 8, 87 | ), 88 | Divider(height: 1, color: Colors.grey.shade300) 89 | ])); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/pages/mine/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'controller.dart'; 4 | 5 | class MineBinding implements Bindings { 6 | @override 7 | void dependencies() { 8 | Get.lazyPut(() => MineController(), fenix: true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/pages/mine/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | import 'package:wechat_flutter/common/entities/index.dart'; 3 | import 'package:wechat_flutter/common/apis/user.dart'; 4 | import 'package:wechat_flutter/common/store/user.dart'; 5 | import 'package:wechat_flutter/common/xresp/xresp.dart'; 6 | import 'package:wechat_flutter/pages/frame/login/view.dart'; 7 | 8 | class MineController extends GetxController { 9 | static MineController get to => Get.find(); 10 | 11 | var loaded = false; 12 | var userId = 0; 13 | var nickName = ""; 14 | var email = ""; 15 | var gender = 0; 16 | var avatarUrl = ""; 17 | 18 | /// 在 widget 内存中分配后立即调用。 19 | @override 20 | void onInit() { 21 | super.onInit(); 22 | // 发送请求 获取个人信息 23 | var data = PersonalInfoRequest(); 24 | UserAPI.personalInfo(data: data).then((personalResp) { 25 | userId = personalResp.userId; 26 | email = personalResp.email; 27 | nickName = personalResp.nickName; 28 | gender = personalResp.gender; 29 | avatarUrl = personalResp.avatarUrl; 30 | loaded = true; 31 | update(); 32 | print("获取个人信息成功: $userId $avatarUrl"); 33 | }).catchError((err) { 34 | var errInfo = "$err"; 35 | if (err.runtimeType == RespBody) { 36 | errInfo = "${err.code} ${err.msg}"; 37 | } 38 | // 显示弹窗 39 | Get.snackbar("personal_err_title".tr, errInfo); 40 | // 删除token 41 | UserStore.to.rmToken(); 42 | // 跳转到登录页面 43 | Get.offAll(() => LoginPage(), transition: Transition.fadeIn); 44 | }); 45 | } 46 | 47 | /// 在 onInit() 之后调用 1 帧。这是进入的理想场所 48 | @override 49 | void onReady() { 50 | super.onReady(); 51 | } 52 | 53 | /// 在 [onDelete] 方法之前调用。 54 | @override 55 | void onClose() { 56 | super.onClose(); 57 | } 58 | 59 | /// dispose 释放内存 60 | @override 61 | void dispose() { 62 | super.dispose(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/pages/mine/index.dart: -------------------------------------------------------------------------------- 1 | library mine; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/mine/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wechat_flutter/common/widgets/network_img.dart'; 5 | 6 | import '../discover/view.dart'; 7 | import 'controller.dart'; 8 | 9 | class MinePage extends StatelessWidget { 10 | const MinePage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text(""), 17 | elevation: 0, 18 | backgroundColor: Colors.white, 19 | systemOverlayStyle: SystemUiOverlayStyle( 20 | statusBarColor: Colors.white, 21 | statusBarBrightness: Brightness.dark), 22 | ), 23 | body: SizedBox( 24 | width: MediaQuery.of(context).size.width, 25 | height: MediaQuery.of(context).size.height, 26 | child: SingleChildScrollView( 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Container( 31 | color: Colors.white, 32 | padding: 33 | EdgeInsets.only(top: 10, bottom: 10, left: 10, right: 10), 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Row( 38 | children: [ 39 | GetBuilder( 40 | builder: (_) => MineController.to.loaded 41 | ? CacheImg(MineController.to.avatarUrl) 42 | : SizedBox()), 43 | SizedBox( 44 | width: 15, 45 | ), 46 | Expanded( 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.start, 49 | children: [ 50 | GetBuilder( 51 | builder: (_) => Text( 52 | MineController.to.nickName, 53 | style: TextStyle( 54 | fontSize: 16, 55 | color: Colors.black, 56 | fontWeight: FontWeight.bold, 57 | ), 58 | )), 59 | SizedBox( 60 | height: 5, 61 | ), 62 | Row(children: [ 63 | GetBuilder( 64 | builder: (_) => Text( 65 | MineController.to.email, 66 | style: TextStyle( 67 | fontSize: 14, 68 | color: Colors.grey.shade600))), 69 | Spacer(), 70 | Image.asset("assets/images/qrcode.png", 71 | width: 20), 72 | SizedBox( 73 | width: 10, 74 | ), 75 | Image.asset("assets/images/next.png", 76 | width: 20), 77 | ]) 78 | ], 79 | )) 80 | ], 81 | ), 82 | Container( 83 | width: 50, 84 | height: 20, 85 | margin: EdgeInsets.only(left: 65, top: 10), 86 | decoration: BoxDecoration( 87 | color: Colors.white, 88 | border: Border.all(color: Colors.grey), 89 | borderRadius: 90 | BorderRadius.all(Radius.circular(13))), 91 | child: Row(children: [ 92 | SizedBox( 93 | width: 5, 94 | ), 95 | Icon( 96 | Icons.add, 97 | size: 14, 98 | color: Colors.grey, 99 | ), 100 | Text("状态", 101 | style: 102 | TextStyle(color: Colors.grey, fontSize: 10)) 103 | ]), 104 | ) 105 | ], 106 | )), 107 | SizedBox(height: 10), 108 | buildItem(() {}, "assets/images/wxpay.png", "支付"), 109 | SizedBox(height: 10), 110 | buildItem(() {}, "assets/images/collect.png", "收藏"), 111 | buildItem(() {}, "assets/images/picture.png", "朋友圈"), 112 | buildItem(() {}, "assets/images/cardpackage.png", "卡包"), 113 | buildItem(() {}, "assets/images/emoij.png", "表情"), 114 | SizedBox(height: 10), 115 | buildItem(() {}, "assets/images/setting.png", "退出"), 116 | ], 117 | )), 118 | )); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/pages/more/add_friend/bindings.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class AddFriendBinding implements Bindings { 4 | @override 5 | void dependencies() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/pages/more/add_friend/controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class AddFriendController extends GetxController { 4 | AddFriendController(); 5 | 6 | /// 在 widget 内存中分配后立即调用。 7 | @override 8 | void onInit() { 9 | super.onInit(); 10 | } 11 | 12 | /// 在 onInit() 之后调用 1 帧。这是进入的理想场所 13 | @override 14 | void onReady() { 15 | super.onReady(); 16 | } 17 | 18 | /// 在 [onDelete] 方法之前调用。 19 | @override 20 | void onClose() { 21 | super.onClose(); 22 | } 23 | 24 | /// dispose 释放内存 25 | @override 26 | void dispose() { 27 | super.dispose(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/pages/more/add_friend/index.dart: -------------------------------------------------------------------------------- 1 | library add_friend; 2 | 3 | export './controller.dart'; 4 | export './bindings.dart'; 5 | export './view.dart'; 6 | -------------------------------------------------------------------------------- /lib/pages/more/add_friend/view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wechat_flutter/common/widgets/search_bar.dart'; 5 | 6 | import 'controller.dart'; 7 | 8 | class AddFriendPage extends StatelessWidget { 9 | AddFriendPage({Key? key}) : super(key: key); 10 | final controller = Get.put(AddFriendController()); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text("添加朋友"), 17 | elevation: 0, 18 | backgroundColor: Colors.white, 19 | systemOverlayStyle: SystemUiOverlayStyle( 20 | statusBarColor: Colors.white, 21 | statusBarBrightness: Brightness.dark), 22 | ), 23 | body: Column(children: [ 24 | SearchBar( 25 | text: "邮箱", 26 | onTap: () { 27 | // controller.search(); 28 | print("点击邮箱号"); 29 | }), 30 | ])); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: wechat_flutter 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.15.1 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | # The following adds the Cupertino Icons font to your application. 34 | # Use with the CupertinoIcons class for iOS style icons. 35 | cupertino_icons: ^1.0.2 36 | json_model: 37 | get: 4.6.1 38 | dio: ^4.0.6 39 | cookie_jar: ^3.0.1 40 | dio_cookie_manager: ^2.0.0 41 | flutter_easyloading: ^3.0.3 42 | shared_preferences: ^2.0.13 43 | desktop_window: ^0.4.0 44 | uuid: ^3.0.6 45 | flutter_screenutil: ^5.5.2 46 | web_socket_channel: ^2.2.0 47 | device_info: ^2.0.3 48 | cached_network_image: ^3.2.0 49 | 50 | dev_dependencies: 51 | flutter_test: 52 | sdk: flutter 53 | 54 | # The "flutter_lints" package below contains a set of recommended lints to 55 | # encourage good coding practices. The lint set provided by the package is 56 | # activated in the `analysis_options.yaml` file located at the root of your 57 | # package. See that file for information about deactivating specific lint 58 | # rules and activating additional ones. 59 | flutter_lints: ^1.0.0 60 | 61 | # For information on the generic Dart part of this file, see the 62 | # following page: https://dart.dev/tools/pub/pubspec 63 | 64 | # The following section is specific to Flutter. 65 | flutter: 66 | # The following line ensures that the Material Icons font is 67 | # included with your application, so that you can use the icons in 68 | # the material Icons class. 69 | uses-material-design: true 70 | 71 | # To add assets to your application, add an assets section, like this: 72 | assets: 73 | - assets/images/ 74 | 75 | # An image asset can refer to one or more resolution-specific "variants", see 76 | # https://flutter.dev/assets-and-images/#resolution-aware. 77 | 78 | # For details regarding adding assets from package dependencies, see 79 | # https://flutter.dev/assets-and-images/#from-packages 80 | 81 | # To add custom fonts to your application, add a fonts section here, 82 | # in this "flutter" section. Each entry in this list should have a 83 | # "family" key with the font family name, and a "fonts" key with a 84 | # list giving the asset and other descriptors for the font. For 85 | # example: 86 | # fonts: 87 | # - family: Schyler 88 | # fonts: 89 | # - asset: fonts/Schyler-Regular.ttf 90 | # - asset: fonts/Schyler-Italic.ttf 91 | # style: italic 92 | # - family: Trajan Pro 93 | # fonts: 94 | # - asset: fonts/TrajanPro.ttf 95 | # - asset: fonts/TrajanPro_Bold.ttf 96 | # weight: 700 97 | # 98 | # For details regarding fonts from package dependencies, 99 | # see https://flutter.dev/custom-fonts/#from-packages 100 | fonts: 101 | - family: wxIconFont 102 | fonts: 103 | - asset: assets/fonts/iconfont.ttf 104 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:wechat_flutter/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------