Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b
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
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8
GIT binary patch
literal 1418
zcmV;51$Fv~P)q
zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+
zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq
z^={4hPQv)y=I|4n+?>7Fim=dxt1
z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT
zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf`
zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_>
z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3
zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF
z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a
z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE
z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62(
zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;?
zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-<
z{s<&cCV_1`^TD^ia9!*mQDq&
zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw
zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv
zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF
z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC
YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838
GIT binary patch
literal 68
zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J
Q1PU{Fy85}Sb4q9e0B4a5jsO4v
literal 0
HcmV?d00001
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..2c783bc
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,49 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Template
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ template
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+
diff --git a/ios/Runner/main.m b/ios/Runner/main.m
new file mode 100644
index 0000000..dff6597
--- /dev/null
+++ b/ios/Runner/main.m
@@ -0,0 +1,9 @@
+#import
+#import
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/ios/RunnerTests/RunnerTests.m b/ios/RunnerTests/RunnerTests.m
new file mode 100644
index 0000000..6d8b0bd
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.m
@@ -0,0 +1,16 @@
+#import
+#import
+#import
+
+@interface RunnerTests : XCTestCase
+
+@end
+
+@implementation RunnerTests
+
+- (void)testExample {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+}
+
+@end
diff --git a/lib/common/Global.dart b/lib/common/Global.dart
new file mode 100644
index 0000000..b379b61
--- /dev/null
+++ b/lib/common/Global.dart
@@ -0,0 +1,55 @@
+
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+import '../network/NetworkConfig.dart';
+
+const int Environment_Dev = 1;
+const int Environment_Pre = 2;
+const int Environment_Online = 3;
+
+class Global {
+ factory Global() => _getInstance();
+
+ static Global get instance => _getInstance();
+ static Global? _instance;
+
+ Global._internal() {
+ // 初始化
+ }
+
+ static Global _getInstance() {
+ if (_instance == null) {
+ _instance = new Global._internal();
+ }
+ return _instance!;
+ }
+
+ static const method = MethodChannel('samples.flutter.dev/battery');
+
+ static bool get isRelease => bool.fromEnvironment("dart.vm.product");
+ static String? flatform_name;
+
+ static Future initialize() async {
+ if (!NetworkConfig.isTest) {
+ NetworkConfig.BASE_URLS = NetworkConfig.BASE_URLS_AI;
+ }
+
+ if (Platform.isIOS) {
+ //ios相关代码
+ flatform_name = 'iOS';
+ } else if (Platform.isAndroid) {
+ //android相关代码
+ flatform_name = 'Android';
+ }
+ WidgetsFlutterBinding.ensureInitialized(); //不加这个强制横/竖屏会报错
+ SystemChrome.setPreferredOrientations([
+ // 强制竖屏
+ DeviceOrientation.portraitUp,
+ DeviceOrientation.portraitDown
+ ]);
+ Global a = Global.instance;
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..ff7c8ee
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,59 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:template/tools/HomePage.dart';
+
+import 'common/Global.dart';
+
+Future main() async {
+ await runZonedGuarded(() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ Global.initialize().then((e) {
+ new Global();
+ runApp(ChatApp());
+ if (Platform.isAndroid) {
+ // 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。
+ SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
+ SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
+ }
+ });
+ }, (error, stackTrace) {});
+}
+
+class ChatApp extends StatefulWidget {
+ const ChatApp({super.key});
+
+ @override
+ State createState() => _ChatAppState();
+}
+
+class _ChatAppState extends State {
+ @override
+ void initState() {
+ // TODO: implement initState
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'AI聊天',
+
+ home: const Homepage(),
+
+ //注册路由
+ routes: {
+ '/HomePage': (BuildContext context) => const Homepage(),
+ },
+
+ debugShowMaterialGrid: false,
+ //显示网格
+ debugShowCheckedModeBanner: false,
+ //去掉右上角的debug
+ builder: EasyLoading.init(),
+ );
+ }
+}
diff --git a/lib/network/BaseEntity.dart b/lib/network/BaseEntity.dart
new file mode 100644
index 0000000..af31442
--- /dev/null
+++ b/lib/network/BaseEntity.dart
@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+class BaseEntity {
+ int? code;
+ int? result;
+ String? message;
+ dynamic data;
+
+ // 构造函数
+ BaseEntity({this.code, this.result, this.message, this.data});
+
+ // 数据解析
+ factory BaseEntity.fromJson(json) {
+ Map responseData = jsonDecode(json);
+ int code = responseData["Code"];
+ int result = responseData["Result"];
+ String message = responseData["Message"]; //错误描述
+ dynamic data = responseData["Data"];
+ return BaseEntity(code: code, result: result, message: message, data: data);
+ }
+
+ // 数据解析
+ factory BaseEntity.PlayfromJson(json) {
+ Map responseData = json;
+ int code = responseData["Code"];
+ // int result = responseData["Result"];
+ String message = responseData["Message"]; //错误描述
+ dynamic data = responseData["Data"];
+ return BaseEntity(code: code, result: 0, message: message, data: data);
+ }
+}
+
+class ErrorEntity {
+ int? code;
+ String? message;
+
+ ErrorEntity({this.code, this.message});
+}
diff --git a/lib/network/DioLogInterceptor.dart b/lib/network/DioLogInterceptor.dart
new file mode 100644
index 0000000..308488d
--- /dev/null
+++ b/lib/network/DioLogInterceptor.dart
@@ -0,0 +1,134 @@
+import 'package:dio/dio.dart';
+
+///日志拦截器
+class DioLogInterceptor extends Interceptor {
+ ///请求前
+ @override
+ void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
+ String requestStr = "\n 请求 - URL:${/*options.baseUrl + */ options.uri}" + " 时间 - :${DateTime.now().millisecondsSinceEpoch}\n";
+ //requestStr += "- HEADER:\n${options.headers.mapToStructureString()}\n";
+ final data = options.data;
+ if (data != null) {
+ if (data is Map)
+ requestStr += "- BODY:\n${data.mapToStructureString()}\n";
+ else if (data is FormData) {
+ final formDataMap = Map()
+ ..addEntries(data.fields)
+ ..addEntries(data.files);
+ requestStr += "- BODY:\n${formDataMap.mapToStructureString()}\n";
+ } else
+ requestStr += "- BODY:\n${data.toString()}\n";
+ }
+ print(requestStr);
+
+ handler.next(options);
+ }
+
+ ///出错前
+ @override
+ void onError(DioError err, ErrorInterceptorHandler handler) {
+ String errorStr = "\n==================== 响应 ====================\n"
+ "- URL:\n${err.requestOptions.path}\n"
+ "- METHOD: ${err.requestOptions.method}\n";
+
+ /* errorStr +=
+ "- HEADER:\n${err.response.headers.map.mapToStructureString()}\n";*/
+ if (err.response != null && err.response!.data != null) {
+ print('╔ ${err.type.toString()}');
+ errorStr += "- ERROR:\n${_parseResponse(err.response!)}\n";
+ } else {
+ errorStr += "- ERRORTYPE: ${err.type}\n";
+ errorStr += "- MSG: ${err.message}\n";
+ }
+ print(errorStr);
+
+ handler.next(err);
+ }
+
+ ///响应前
+ @override
+ void onResponse(Response response, ResponseInterceptorHandler handler) {
+ String responseStr = "\n 响应 - URL:${response.requestOptions.uri}" + " 时间 - :${DateTime.now().millisecondsSinceEpoch}\n";
+
+ /* responseStr += "- HEADER:\n{";
+ response.headers.forEach(
+ (key, list) => responseStr += "\n " + "\"$key\" : \"$list\",");
+ responseStr += "\n}\n";*/
+ //responseStr += "- STATUS: ${response.statusCode}\n";
+
+ if (response.data != null) {
+ responseStr += "- BODY:\n ${_parseResponse(response)}";
+ }
+ printWrapped(responseStr);
+
+ handler.next(response);
+ }
+
+ void printWrapped(String text) {
+ final pattern = new RegExp('.{1,800}'); // 800 is the size of each chunk
+ pattern.allMatches(text).forEach((match) => print(match.group(0)));
+ }
+
+ String _parseResponse(Response response) {
+ String responseStr = "";
+ var data = response.data;
+ if (data is Map)
+ responseStr += data.mapToStructureString();
+ else if (data is List)
+ responseStr += data.listToStructureString();
+ else
+ responseStr += response.data.toString();
+
+ return responseStr;
+ }
+}
+
+///Map拓展,MAp转字符串输出
+extension Map2StringEx on Map {
+ String mapToStructureString({int indentation = 2}) {
+ String result = "";
+ String indentationStr = " " * indentation;
+ if (true) {
+ result += " {";
+ this.forEach((key, value) {
+ if (value is Map) {
+ var temp = value.mapToStructureString(indentation: indentation + 2);
+ result += "$indentationStr" + "\"$key\":$temp,";
+ } else if (value is List) {
+ result += "$indentationStr" + "\"$key\":${value.listToStructureString(indentation: indentation + 2)},";
+ } else {
+ result += "$indentationStr" + "\"$key\":\"$value\",";
+ }
+ });
+ result = result.substring(0, result.length - 1);
+ result += indentation == 2 ? "}" : "\n${" " * (indentation - 1)}}";
+ }
+
+ return result;
+ }
+}
+
+///List拓展,List转字符串输出
+extension List2StringEx on List {
+ String listToStructureString({int indentation = 2}) {
+ String result = "";
+ String indentationStr = " " * indentation;
+ if (true) {
+ result += "$indentationStr[";
+ this.forEach((value) {
+ if (value is Map) {
+ var temp = value.mapToStructureString(indentation: indentation + 2);
+ result += "\n$indentationStr" + "\"$temp\",";
+ } else if (value is List) {
+ result += value.listToStructureString(indentation: indentation + 2);
+ } else {
+ result += "\n$indentationStr" + "\"$value\",";
+ }
+ });
+ result = result.substring(0, result.length - 1);
+ result += "\n$indentationStr]";
+ }
+
+ return result;
+ }
+}
diff --git a/lib/network/NetworkConfig.dart b/lib/network/NetworkConfig.dart
new file mode 100644
index 0000000..11d3d44
--- /dev/null
+++ b/lib/network/NetworkConfig.dart
@@ -0,0 +1,38 @@
+
+
+class NetworkConfig {
+ static String ServerDomain_Online = BASE_URLS[SELECT_INDEX];
+ static String deviceID = ""; //设备ID
+ static String systemVersion = ""; //设备ID
+
+ /// 选择哪个域名做请求
+ static int SELECT_INDEX = 0;
+
+ static List BASE_URLS = [
+ "http://127.0.0.1:7860/",
+ "http://127.0.0.1:7860/",
+ "http://127.0.0.1:7860/",
+ ];
+
+ static List BASE_URLS_AI = [
+ "http://127.0.0.1:7860/",
+ ];
+
+
+ static bool isTest = true;
+
+ static String token = "";
+ static String userToken = ""; //用户登录验签
+ static String AppId = "1";
+ static String BossId = "";
+ static String userId = "";
+ static String userName = "";
+ static String Version = "1.0.0";
+ static String Language = "en";
+
+
+
+ static const String login = "login"; //登录
+
+
+}
diff --git a/lib/network/RequestCenter.dart b/lib/network/RequestCenter.dart
new file mode 100644
index 0000000..d534d17
--- /dev/null
+++ b/lib/network/RequestCenter.dart
@@ -0,0 +1,374 @@
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+import 'package:dio/dio.dart';
+
+import 'BaseEntity.dart';
+import 'DioLogInterceptor.dart';
+import 'NetworkConfig.dart';
+
+enum RequestMethod {
+ Post,
+ Get,
+}
+
+const RequestMethodValues = {
+ RequestMethod.Get: "GET",
+ RequestMethod.Post: "POST",
+};
+
+class RequestCenter {
+ factory RequestCenter() => _getInstance();
+
+ static RequestCenter get instance => _getInstance();
+ static RequestCenter? _instance;
+
+ static String signKey = "5159d59637fe1b79ba789e87443e8282";
+
+ Dio? _dio, _dioLog;
+
+ RequestCenter._internal() {
+ setup();
+ }
+
+ static RequestCenter _getInstance() {
+ _instance ??= RequestCenter._internal();
+ //域名不一致,重新初始化
+ if (_instance!._dio != null && NetworkConfig.ServerDomain_Online != _instance!._dio!.options.baseUrl) {
+ _instance!._dio!.options.baseUrl = NetworkConfig.ServerDomain_Online;
+ }
+ return _instance!;
+ }
+
+ void setup() {
+ // 初始化
+ if (null == _dio) {
+ _dio = Dio(BaseOptions(
+ baseUrl: NetworkConfig.ServerDomain_Online,
+ sendTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 20),
+ connectTimeout: const Duration(seconds: 20),
+ contentType: Headers.jsonContentType,
+ responseType: ResponseType.json));
+ _dio!.interceptors.add(DioLogInterceptor());
+ /* _dio.interceptors.add(new ResponseInterceptors());*/
+ _dioLog = Dio(BaseOptions(
+ sendTimeout: const Duration(seconds: 10),
+ receiveTimeout: const Duration(seconds: 20),
+ connectTimeout: const Duration(seconds: 20),
+ contentType: Headers.jsonContentType,
+ responseType: ResponseType.json));
+ _dioLog!.interceptors.add(DioLogInterceptor());
+
+ // if (NetworkConfig.isAgent) {
+ // (_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
+ // client.findProxy = (uri) {
+ // return "PROXY 192.168.1.231:8888";
+ // };
+ // //抓Https包设置
+ // client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
+ // };
+ // }
+ }
+ }
+
+ //特殊处理网络请求默认为post
+ Future request1(path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "userId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ Map parmetersSign = sign(parmeters);
+ try {
+ /*由于服务器是表单结构*/
+ FormData formData = FormData.fromMap(parmetersSign);
+ Response response = await _dio!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.fromJson(response.data);
+ success(entity);
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ } catch (e) {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ }
+
+ Future requestLog(
+ path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "userId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ Map parmetersSign = sign(parmeters);
+ try {
+ /*由于服务器是表单结构*/
+ FormData formData = FormData.fromMap(parmetersSign);
+ Response response = await _dioLog!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.fromJson(response.data);
+ success(entity);
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ } catch (e) {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ }
+
+ // 网络请求默认为post
+ Future request(path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "userId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ //签名加密
+ Map parmetersSign = sign(parmeters);
+ try {
+ FormData formData = FormData.fromMap(parmetersSign);
+ Response response = await _dio!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.fromJson(response.data);
+ success(entity);
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ } catch (e) {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ }
+
+
+
+ // 网络请求默认为post
+ Future requestPay(
+ path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "userId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ Map parmetersSign = sign(parmeters);
+ try {
+ FormData formData = FormData.fromMap(parmetersSign);
+ Response response = await _dio!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.fromJson(response.data);
+ success(entity);
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "未知错误"));
+ return null;
+ }
+ } catch (e) {
+ ErrorEntity(code: -1, message: "未知错误");
+ return null;
+ }
+ }
+
+ // 网络请求默认为post
+ Future requestPlay(
+ path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "UserId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ Map parmetersSign = sign(parmeters);
+ try {
+ FormData formData = FormData.fromMap(parmetersSign);
+ print("Authorization==${NetworkConfig.token}");
+ //用户验签
+ _dio!.options.headers = {"Authorization": NetworkConfig.token};
+ Response response = await _dio!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.PlayfromJson(response.data);
+ success(entity);
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ } catch (e) {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ }
+
+ // 网络请求默认为post (网页)
+ Future requestWeb(
+ path, Map parmeters, Function(BaseEntity dataEntity) success, Function(ErrorEntity errorEntity) error,
+ {RequestMethod? method}) async {
+ Map headers = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "UserId": NetworkConfig.userId,
+ "Version": NetworkConfig.Version,
+ "Language": NetworkConfig.Language
+ };
+ parmeters.addAll(headers);
+ //签名加密
+ Map parmetersSign = sign(parmeters);
+ try {
+ /*由于服务器是表单结构*/
+ FormData formData = FormData.fromMap(parmetersSign);
+ Response response = await _dio!.post(path, data: formData);
+ if (response != null && response.statusCode == 200) {
+ BaseEntity entity = BaseEntity.fromJson(response.data);
+ Map parmeters = {
+ "AppId": NetworkConfig.AppId,
+ /*"BossId": NetworkConfig.BossId,*/
+ "UserId": NetworkConfig.userId,
+ "Token": entity.message
+ };
+ Map p1 = sign(parmeters);
+ parmeters.addAll(p1);
+ //data 信息添加用户信息
+ entity.data = gettoken(parmeters);
+ success(entity);
+
+ return entity;
+ } else {
+ error(ErrorEntity(code: -1, message: "Network Anomaly"));
+ return null;
+ }
+ } catch (e) {
+ ErrorEntity(code: -1, message: "Network Anomaly");
+ return null;
+ }
+ }
+
+ // 捕获异常的错误信息
+ ErrorEntity _getErrorMsg(DioError error) {
+ switch (error.type) {
+ case DioErrorType.cancel:
+ {
+ return ErrorEntity(code: -1, message: "请求取消");
+ }
+ break;
+ case DioErrorType.connectionTimeout:
+ {
+ return ErrorEntity(code: -1, message: "连接超时");
+ }
+ break;
+ case DioErrorType.sendTimeout:
+ {
+ return ErrorEntity(code: -1, message: "请求超时");
+ }
+ break;
+ case DioErrorType.receiveTimeout:
+ {
+ return ErrorEntity(code: -1, message: "响应超时");
+ }
+ break;
+ case DioErrorType.badResponse:
+ {
+ try {
+ int errCode = error.response!.statusCode!;
+ String errorMsg = error.response!.statusMessage!;
+ return ErrorEntity(code: errCode, message: errorMsg);
+ } on Exception catch (_) {
+ return ErrorEntity(code: -1, message: "Network Anomaly");
+ }
+ }
+ break;
+ default:
+ {
+ return ErrorEntity(code: -1, message: "Network Anomaly");
+ }
+ }
+ }
+
+ Map sign(Map parmeters) {
+ List keys = parmeters.keys.toList();
+ // key排序
+ keys.sort((a, b) {
+ List al = a.codeUnits;
+ List bl = b.codeUnits;
+ for (int i = 0; i < al.length; i++) {
+ if (bl.length <= i) return 1;
+ if (al[i] > bl[i]) {
+ return 1;
+ } else if (al[i] < bl[i]) return -1;
+ }
+ return 0;
+ });
+ Map treeMap = Map();
+ keys.forEach((element) {
+ treeMap[element] = parmeters[element];
+ });
+ String data = "";
+ for (dynamic str in treeMap.values) {
+ if (str != null) {
+ data = data + str.toString();
+ }
+ }
+ treeMap["sign"] = generateMd5(data + NetworkConfig.userToken);
+ return treeMap;
+ }
+
+ String gettoken(Map parmeters) {
+ List keys = parmeters.keys.toList();
+ // key排序
+ keys.sort((a, b) {
+ List al = a.codeUnits;
+ List bl = b.codeUnits;
+ for (int i = 0; i < al.length; i++) {
+ if (bl.length <= i) return 1;
+ if (al[i] > bl[i]) {
+ return 1;
+ } else if (al[i] < bl[i]) return -1;
+ }
+ return 0;
+ });
+ Map treeMap = Map();
+ keys.forEach((element) {
+ treeMap[element] = parmeters[element];
+ });
+ String data = "";
+ treeMap.forEach((key, value) {
+ if (data == "") {
+ data = key + "=" + value.toString();
+ } else {
+ data = data + "&" + key + "=" + value.toString();
+ }
+ });
+ return data;
+ }
+
+ String generateMd5(String data) {
+ return md5.convert(utf8.encode(data)).toString();
+ }
+}
diff --git a/lib/tools/HomePage.dart b/lib/tools/HomePage.dart
new file mode 100644
index 0000000..8473dee
--- /dev/null
+++ b/lib/tools/HomePage.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/material.dart';
+
+class Homepage extends StatefulWidget {
+ const Homepage({super.key});
+
+ @override
+ State createState() => _HomepageState();
+}
+
+class _HomepageState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ child: Text("你好"),
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..2774e47
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,90 @@
+name: template
+description: "A new Flutter project."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: '>=3.4.1 <4.0.0'
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ dio: ^5.4.3+1
+ cupertino_icons: ^1.0.6
+ flutter_easyloading: ^3.0.5
+ crypto: ^3.0.3
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^3.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 0000000..6cf68dc
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:template/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/web/favicon.png b/web/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1
GIT binary patch
literal 917
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7
zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2
zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK
zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o
z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo
zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8
zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr
zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf
zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od
z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz
oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM
literal 0
HcmV?d00001
diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png
new file mode 100644
index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa
GIT binary patch
literal 5292
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
literal 0
HcmV?d00001
diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png
new file mode 100644
index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6
GIT binary patch
literal 8252
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