From 557f90018779305fde8a62c75fde6455d3e65771 Mon Sep 17 00:00:00 2001 From: phoenix--zhang <471155679@qq.com> Date: Mon, 6 Jul 2026 10:45:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E7=B1=BB=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/other/pricary.png | Bin 0 -> 1163 bytes assets/other/web_site.png | Bin 0 -> 1463 bytes lib/common/api/api_request.dart | 573 +++++++++++++++++++++++ lib/common/api/api_string.dart | 53 +++ lib/common/api/ny_network_intercept.dart | 141 ++++++ lib/common/api/user_request.dart | 41 ++ lib/common/api/video_request.dart | 0 lib/common/help/help_encrypt.dart | 155 ++++++ lib/common/help/help_image.dart | 4 +- lib/common/help/help_log.dart | 146 ++++++ lib/common/help/help_time.dart | 55 +++ lib/common/help/help_token.dart | 228 +++++++++ lib/common/model/novyronst_user.dart | 60 +++ lib/common/model/parse_model.dart | 57 +++ lib/common/model/register_model.dart | 18 + lib/pages/my/pages/my_page.dart | 59 ++- lib/tools/util/ny_device_info.dart | 177 +++++++ lib/tools/widgets/app_toast_util.dart | 139 ++++++ pubspec.lock | 120 +++++ pubspec.yaml | 3 +- 20 files changed, 2026 insertions(+), 3 deletions(-) create mode 100644 assets/other/pricary.png create mode 100644 assets/other/web_site.png create mode 100644 lib/common/api/api_request.dart create mode 100644 lib/common/api/api_string.dart create mode 100644 lib/common/api/ny_network_intercept.dart create mode 100644 lib/common/api/user_request.dart create mode 100644 lib/common/api/video_request.dart create mode 100644 lib/common/help/help_encrypt.dart create mode 100644 lib/common/help/help_log.dart create mode 100644 lib/common/help/help_time.dart create mode 100644 lib/common/help/help_token.dart create mode 100644 lib/common/model/novyronst_user.dart create mode 100644 lib/common/model/parse_model.dart create mode 100644 lib/common/model/register_model.dart create mode 100644 lib/tools/util/ny_device_info.dart create mode 100644 lib/tools/widgets/app_toast_util.dart diff --git a/assets/other/pricary.png b/assets/other/pricary.png new file mode 100644 index 0000000000000000000000000000000000000000..729f30d377616d2d9d27c24b8d0786f1fd04bd3a GIT binary patch literal 1163 zcmV;61a$j}P)r>Dk3T(DiL@PxZM{2VY|6&uGzhJm)w3ckIW@un)~nVdvD+F zYzqZ>czD=RFZ;^&ly#KtD%-(kFjF>FHc@u3?5@>n&5HcT)~YWkVJO6bvMx49kRptg z4V6&Sr@oXcknxxvv^d+$2wJF2kIEFtc+E@bVblcZMK@XAv?P!chw^B03TMU6tylV!^GiBG1o&TaZ98hL|6a zFCZ@txq^N$F8pmlGlfur4!vu7&lRfU8q2A^Qi1$G9=#4(1OGP!)nciVUV+p+>>YHb zi`6+`cKo62CrU=bgDA=;#BHl^*z_ULs@dcrCep`}5QsrDO4;vm34x>_6@v!QF(cMf zpFk*MN;b>HZYm~H02&pARd52AOl6IhFqKuVx*cwV69@Hs^d zsB0FhYT0NBsVw%~=Dk$2efg;7)-BEOB$3WRHW#66mDA-r?zDwvz$ffyvLBz^p^o+k) zUZ?lTdeXf94DBr!<~nq@2+M>4A`o+b5a9*_#-FS}O;7m%J)F*SzGHWZ9b!Fdy@ASw zUf|wj))u)wR(qUf)lHYS1+OfDiozbzQ$g4Uagp9n7F91CvPZ;Bla`<%KY<`CN?6$9sFoqe2*c6F8rW9!7mV*VEP@sDx^0=o! zchGStvCLiDoLNz~;8+L7)W+69BP4u={AVtZIn*PG^q-ME=)|q0he#=(P=lepSO{cd z(G8j{l07D8k+dGo#f)3sqM0Ju<8dC{dpclAQ~ZD>OFE)UQ}mFx9=7Xz4_o_<);{FT dr>VyT{sRi~3C(M_%ohLv002ovPDHLkV1jRN5|#h} literal 0 HcmV?d00001 diff --git a/assets/other/web_site.png b/assets/other/web_site.png new file mode 100644 index 0000000000000000000000000000000000000000..355c07569f883e05cc8b68976888ce0f6df58d49 GIT binary patch literal 1463 zcmV;o1xWgdP)V}6vv;VfC7Vn0)qleS9nswbKuy8UrABT zTN2cgqN1Xppc0q2JVhl9Po=_E3J3@Y2rvkUl+NEflaAlMvEO>$u9J9v(j?yRo&Ua% z+1b(bNXgC3&8GYJ*p)3;`mQ{3Ws~j)*REW-GI8b7m5W}lcda?Fw`d0^41{>%%7E^V zAR5kHIdwu^&D$v{khhp2RpLBk&ZrVfv?x&^Z#6qqxzRcRD>{*^sV0yU2l3@^S|4!& z5a)*zX%es8h!?!&tfM$!T6eF(Ew=*sn6pl9B{2;xUW+NQ1r2Gv#R))zVOW?76s7V) z#gpJ_!GeZV;7ko>!~5HS3Y-D7fdxILg2eRI3Ic6WA;lI1>Qf;_9|D!sp5_t!>@K{a z52&6f(B%I~t7UUl;-JO8>;FGpu~LLK|xoziou>|g2z#Jc}o*`wu~73jkmyYhuP zqAP*^bLAD;C4|m|YFi*!%4@1zNdsI6vF9J<{@+|Vq)M<>Xj34>mUpCcCsgn){RzrA z=Agl``_z(18`KUEN2*{dGreW&QQ7yxen(Un0@wIJi-Y}Av>{M<`aIsi9iCGqQVc3B zi3WH3B%FKS9~0B}*EAQ&TA-9TAE~kutltr~rNFjW&>eq`_rh^Fx7MZASPS%$>NkkW zjQ#HyY@6gVF%zm=%eMbNrn(!3q+o`nKuG00``{_Z739{VNz=z{c`y2XM0HEr@%Lha zr9dOLjGG@v%ZLxLb-{8pE#r{-1N^mQOjUe~F>7;K34|xcE6<&Wy^52 zSj5n3&&Eoi5nF~hJhpK6YVuyi#pKxv;_!^8ZEViPLZFaY0ubg%*pCPAU!>=|a81r< znzf>*q;J6|u@H!xd&)BSQ*Yv8Qsa;#WxQR=#6lomaVaxNE$RvB!M+-ic*eUE7Nm(S zyfVvlgmFQoEVU?BH6~Y}9JgtTMj)-Fsmbk80P*bgx$jv2^w`_zmBn$4K|Bxg+CdDf&kQ6txaq>PcuE-`I> z%{9%e7A~e~dsqm>&nso7ocBEu3xW7eNX>h#rKRW#`%41xexU4jnT0@7hn7_NoCciM zZMX#;Qzljx6yI>FFNRy%HtG3`EvKw!nGU(RSfzR1q4kXFR+RIev?#Vfm=Rky?_+=( zD}i`l48-A`=iJb)`)BD zv13fE2UxbG@x-|4kEs3?B1Y4p#WT|h+csj`zmbvk0P9x`be{773Rer3#9bqm@d5)% zTL;ehhHb;O56KW~fx;jJ-Y^_dB^1vr5a-35^YLxr6VN-VTMpKn4`OI*bVnGxAT{n# zNp2O=dlk;{fvrRAks6sn9PQO}c!&#WG7rIPXiG&l_lw*vWCbVBPbQkW*^HfhcD zD0;K{LF*|}m>y@Q?q!0d3jG92figu`g?5s+Aw8`5Jwn^>Z0SkfIO{r1;BU2cZVueA R0z3c!002ovPDHLkV1kj-vK#;a literal 0 HcmV?d00001 diff --git a/lib/common/api/api_request.dart b/lib/common/api/api_request.dart new file mode 100644 index 0000000..2e44200 --- /dev/null +++ b/lib/common/api/api_request.dart @@ -0,0 +1,573 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:novyronst/common/api/api_string.dart'; +import 'package:novyronst/common/api/ny_network_intercept.dart'; +import 'package:novyronst/common/help/help_token.dart'; +import 'package:novyronst/tools/widgets/app_toast_util.dart'; + + +/// HTTP 请求工具类(单例) +class AppService { + static AppService? _instance; + late Dio _dio; + + static AppService get instance { + _instance ??= AppService._internal(); + return _instance!; + } + + AppService._internal() { + _initDio(); + } + + Dio get dio => _dio; + + /// 初始化 Dio 配置 + void _initDio() { + _dio = Dio(BaseOptions( + baseUrl: ApiString.baseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 15), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + responseType: ResponseType.json, + )); + + // 添加自定义拦截器 + _dio.interceptors.add(NyNetworkIntercept()); + + // 开发环境添加日志 + // if (!kReleaseMode) { + // _dio.interceptors.add(TalkerDioLogger( + // settings: TalkerDioLoggerSettings( + // printRequestHeaders: true, + // printRequestData: true, + // printResponseData: true, + // printResponseHeaders: false, + // ), + // )); + // } + } + + /// 更新配置 + void updateBaseOptions(BaseOptions options) { + _dio.options = options; + } +} + +/// ============ 请求方法封装 ============ + +class ApiRequest { + static final Dio _dio = AppService.instance.dio; + + /// GET 请求(带重试) + static Future> get( + String path, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + int? retry, + }) async { + try { + final response = await _executeRequest( + () => _dio.get( + path, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ), + ); + return HttpResult.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// POST 请求(带重试) + static Future> post( + String path, { + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + int? retry, + }) async { + try { + final response = await _executeRequest( + () => _dio.post( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ), + ); + return HttpResult.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + + /// 文件上传(带重试) + static Future> fileUpload( + String path, { + required File file, + String fieldName = 'file', + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + int? retry, + }) async { + try { + final formData = FormData.fromMap({ + fieldName: await MultipartFile.fromFile(file.path), + }); + + final response = await _executeRequest( + () => _dio.post( + path, + data: formData, + queryParameters: queryParameters, + options: options ?? Options(contentType: 'multipart/form-data'), + cancelToken: cancelToken, + onSendProgress: onSendProgress, + ), + ); + return HttpResult.fromResponse(response); + } on DioException catch (e) { + return _handleError(e); + } catch (e) { + return HttpResult.error(e.toString()); + } + } + + /// ============ 分页请求(原项目的 fetchPage) ============ + static Future> fetchPage( + String url, { + int? page, + int? pageSize, + Map? extraParams, + required T Function(dynamic json) fromJson, + bool error = true, + String method = 'get', + int? retry, + }) async { + try { + final params = { + 'current_page': page?.toString() ?? '1', + 'page_size': pageSize?.toString() ?? '20', + ...?extraParams, + }; + + Response response; + if (method.toLowerCase() == 'get') { + response = await _executeRequest( + () => _dio.get(url, queryParameters: params), + ); + } else { + response = await _executeRequest( + () => _dio.post(url, data: params), + ); + } + + if (response.statusCode != 200) { + throw Exception("HTTP ${response.statusCode}"); + } + + final data = response.data; + if (data is Map) { + final base = BasePageResponse.fromJson(data); + if (!base.success) { + if (error) { + // 这里可以触发 Toast + AppToastUtil.showError(base.msg); + } + throw Exception(base.msg); + } + + final result = base.data; + if (result is List) { + return result.map((e) => fromJson(e)).toList(); + } else if (result is Map && result['list'] is List) { + return (result['list'] as List).map((e) => fromJson(e)).toList(); + } else { + throw Exception("data error"); + } + } else { + throw Exception("format error"); + } + } catch (e) { + // _handleError(); + rethrow; + } + } + + /// 直接执行请求,不重试 + static Future _executeRequest( + Future Function() requestFn, + ) async { + try { + final response = await requestFn(); + + // 如果状态码不是 200,直接抛出异常 + if (response.statusCode != 200) { + throw DioException( + requestOptions: response.requestOptions, + response: response, + type: DioExceptionType.badResponse, + error: 'HTTP ${response.statusCode}', + ); + } + + return response; + } catch (e) { + // 直接重新抛出异常,不重试 + rethrow; + } + } + + // ============ 重试机制 ============ + static Future _retryRequest( + Future Function() requestFn, { + int? retries, + }) async { + int attempt = 0; + final maxRetries = retries ?? 2; + + while (attempt <= maxRetries) { + try { + final response = await requestFn(); + if (response.statusCode == 200) { + return response; + } + // 401/402 特殊处理,由拦截器处理 + if (response.statusCode == 401 || response.statusCode == 402) { + if (attempt == maxRetries) { + throw DioException( + requestOptions: response.requestOptions, + response: response, + type: DioExceptionType.badResponse, + error: 'errorCode:${response.statusCode}', + ); + } + } + if (attempt == maxRetries) { + throw DioException( + requestOptions: response.requestOptions, + response: response, + type: DioExceptionType.badResponse, + error: 'HTTP ${response.statusCode}', + ); + } + } catch (e) { + if (attempt == maxRetries) { + rethrow; + } + } + attempt++; + await Future.delayed(const Duration(milliseconds: 300)); + } + throw Exception("retry failed (unreachable)"); + } + + // ============ 错误处理 ============ + + static HttpResult _handleError(DioException e) { + final Response? resp = e.response; + + // 尝试从响应中解析错误信息 + if (resp != null && resp.data != null) { + try { + final parsed = HttpResult.fromResponse(resp); + if (!parsed.success) { + // 401/402 错误,触发重新登录 + if (resp.statusCode == 401 || resp.statusCode == 402) { + HelpToken().update(); + if (resp.statusCode == 402) { + AppToastUtil.showError('账号已在其他设备登录,请重新登录'); + } + } + return parsed; + } + } catch (_) {} + } + + String message = _getErrorMessage(e); + return HttpResult.error(message, httpStatus: resp?.statusCode); + } + + + static String _getErrorMessage(DioException e) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + return '连接超时,请稍后重试'; + case DioExceptionType.sendTimeout: + return '发送超时,请稍后重试'; + case DioExceptionType.receiveTimeout: + return '接收超时,请稍后重试'; + case DioExceptionType.badResponse: + final statusCode = e.response?.statusCode; + if (statusCode == 401) { + HelpToken().update(); + return '未授权,请重新登录'; + } + if (statusCode == 402) { + HelpToken().update(); + return '账号已在其他设备登录,请重新登录'; + } + return _handleStatusCode(statusCode); + case DioExceptionType.cancel: + return '请求已取消'; + case DioExceptionType.connectionError: + return '网络连接失败,请检查网络设置'; + default: + return '网络错误,请稍后重试'; + } + } + + static String _handleStatusCode(int? statusCode) { + switch (statusCode) { + case 400: + return '请求错误'; + case 403: + return '拒绝访问'; + case 404: + return '请求资源不存在'; + case 500: + return '服务器内部错误'; + case 502: + return '网关错误'; + case 503: + return '服务不可用'; + default: + return '请求失败'; + } + } +} + + +/// 统一响应:业务成功见 [isSuccess] +class HttpResult { + final int? status; + final bool success; + final String? message; + final T? data; + final int? errorCode; + + const HttpResult({ + this.status, + this.success = false, + this.message, + this.data, + this.errorCode, + }); + + bool get isSuccess => success; + + factory HttpResult.fromResponse(Response response) { + try { + final int httpStatus = response.statusCode ?? 0; + final dynamic raw = response.data; + final Map? root = _jsonMap(raw); + + // 明确失败包:{ "success": false, "error": { ... } } + if (root != null && root['success'] == false) { + final payload = ApiErrorPayload.tryParse(root); + return HttpResult( + status: httpStatus, + success: false, + message: payload?.message ?? root['message']?.toString() ?? '请求失败', + errorCode: payload?.code, + data: null, + ); + } + + // HTTP 2xx:body 即为业务数据 + if (httpStatus >= 200 && httpStatus < 300) { + return HttpResult( + status: httpStatus, + success: true, + message: null, + data: raw as T?, + errorCode: null, + ); + } + + // 非 2xx:尽量从 body 里拆 error + if (root != null) { + final payload = ApiErrorPayload.tryParse(root); + if (payload != null) { + return HttpResult( + status: httpStatus, + success: false, + message: payload.message, + errorCode: payload.code, + data: null, + ); + } + } + + return HttpResult( + status: httpStatus, + success: false, + message: _handleStatusCode(httpStatus), + data: null, + ); + } catch (e) { + return HttpResult( + status: response.statusCode, + success: false, + message: '数据解析失败: $e', + data: null, + ); + } + } + + factory HttpResult.error(String message, {int? httpStatus}) { + return HttpResult( + success: false, + status: httpStatus, + message: message, + ); + } + + factory HttpResult.networkError() { + return HttpResult( + success: false, + status: -1, + message: '网络连接失败,请检查网络后重试', + ); + } + + // ============ 私有方法 ============ + static Map? _jsonMap(dynamic value) { + if (value is Map) return value; + if (value is Map) { + try { + return value.cast(); + } catch (_) { + return Map.from(value as Map); + } + } + return null; + } + + static String _handleStatusCode(int? statusCode) { + switch (statusCode) { + case 400: + return '请求错误'; + case 401: + return '未授权,请重新登录'; + case 403: + return '拒绝访问'; + case 404: + return '请求资源不存在'; + case 500: + return '服务器内部错误'; + case 502: + return '网关错误'; + case 503: + return '服务不可用'; + default: + return '请求失败'; + } + } +} + +/// 后台错误体:`{ "error": { "code", "details", "message" }, "success": false }` +class ApiErrorPayload { + final int? code; + final dynamic details; + final String? message; + + const ApiErrorPayload({this.code, this.details, this.message}); + + static ApiErrorPayload? tryParse(Map? root) { + if (root == null) return null; + final err = root['error']; + if (err is! Map) return null; + final m = _jsonMap(err); + if (m == null) return null; + return ApiErrorPayload( + code: _parseInt(m['code']), + details: m['details'], + message: m['message']?.toString(), + ); + } + + static Map? _jsonMap(dynamic value) { + if (value is Map) return value; + if (value is Map) { + try { + return value.cast(); + } catch (_) { + return Map.from(value as Map); + } + } + return null; + } + + static int? _parseInt(dynamic v) { + if (v == null) return null; + if (v is int) return v; + if (v is num) return v.toInt(); + return int.tryParse(v.toString()); + } +} + +/// 分页响应模型(原项目的 GlimzoGetxBaseResponse 改造) +class BasePageResponse { + final int code; + final String msg; + final T? data; + + BasePageResponse({ + required this.code, + required this.msg, + this.data, + }); + + bool get success => code == 200; + + factory BasePageResponse.fromJson(Map json) { + return BasePageResponse( + code: json['code'] ?? -1, + msg: json['msg'] ?? '', + data: json['data'], + ); + } +} + +/// Map 数据提取工具(原项目的 GlimzoMapRes 改造) +class MapRes { + static List getList({ + required Map mapData, + required String alias, + required T Function(Map) fromJson, + }) { + final rawList = mapData[alias]; + if (rawList is List) { + return rawList + .whereType>() + .map(fromJson) + .toList(); + } + return []; + } + + static T toModel({ + required Map json, + required T Function(Map) fromJson, + }) { + return fromJson(json); + } +} \ No newline at end of file diff --git a/lib/common/api/api_string.dart b/lib/common/api/api_string.dart new file mode 100644 index 0000000..9259c15 --- /dev/null +++ b/lib/common/api/api_string.dart @@ -0,0 +1,53 @@ + +// https://api-glimzodf.glimzodf.com/glimzodf/ + +class ApiString { + static const String baseUrl = "https://api-glimzodf.glimzodf.com/"; + + static const String appName = 'Novyronst'; + + static String webPrefix = "glimzodf.com"; + + static String webIndex = "https://$webPrefix"; + + static String webPrivacy = "https://$webPrefix/private"; + + static String webAgreement = "https://$webPrefix/user_policy"; + + static String webmemberAgreement = "https://$webPrefix/member_ship_agreement"; +} + +class ApiStringUser { + static const String login = "/customer/login"; + // /customer/register + static const String register = "/customer/register"; + // /customer/info + static const String userInfo = "/customer/info"; + + static const String eventCreateHistory = "/createHistory"; + static const String eventUserEnterApp = "/customer/enterTheApp"; + static const String eventUserOnline = "/customer/onLine"; + static const String eventUserLeaveApp = "/customer/leaveApp"; + static const String eventPlaySeconds = "/uploadHistorySeconds"; + static const String eventWatchTime = "/watchShortDuration"; + static const String eventReport = "/event/add"; +} + +class ApiStringVideo { + + static const String getRecommandsList = "/getRecommands"; + static const String getVideoDetail = "/getVideoDetails"; + + static const String getHomeModules = "/home/all-modules"; + static const String getRankingList = "/homeRanking"; + static const String getCategoriesList = "/getCategories"; + static const String getCategoriesDetail = "/videoList"; + static const String getWaterflowList = "/newShortPlay"; + + static const String getSearchHots = "/search/hots"; + static const String getSearch = "/search"; + static const String collect = "/collect"; + static const String cancelCollect = "/cancelCollect"; + static const String getCollections = "/myCollections"; + static const String getHistories = "/myHistorys"; +} \ No newline at end of file diff --git a/lib/common/api/ny_network_intercept.dart b/lib/common/api/ny_network_intercept.dart new file mode 100644 index 0000000..010275c --- /dev/null +++ b/lib/common/api/ny_network_intercept.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; +import 'package:novyronst/common/help/help_time.dart'; +import 'package:novyronst/common/help/help_token.dart'; + +import '../../tools/util/ny_device_info.dart'; +import '../help/help_encrypt.dart'; + + +/// 网络请求拦截器 +class NyNetworkIntercept extends Interceptor { + final bool enableLog = !kReleaseMode; + final Logger _logger = Logger(); + + @override + Future onRequest(RequestOptions options, RequestInterceptorHandler handler) async { + // 添加公共请求头 + await _addRequestHeaders(options); + + // 打印请求日志 + _logInfo("🔶 REQUEST [${options.method.toUpperCase()}] ${options.uri}"); + _logDebug("Headers: ${options.headers}"); + if (options.data != null) { + _logDebug("Payload: ${options.data}"); + } + + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _logInfo("🟢 RESPONSE [${response.statusCode}] ${response.requestOptions.uri}"); + // _logDebug("Body: ${response.data}"); + + // 处理 401 / 402 Token 过期 + if (response.statusCode == 401 || response.statusCode == 402) { + // 触发 Token 刷新 + HelpToken().update(); + if (response.statusCode == 402) { + _handleError('errorCode:${response.statusCode}', true); + } + handler.reject(DioException( + requestOptions: response.requestOptions, + response: response, + type: DioExceptionType.badResponse, + error: 'errorCode:${response.statusCode}', + )); + return; + } + + // Release 模式解密响应体 + if (kReleaseMode && response.data is String) { + try { + final deStr = HelpEncrypt.deStr(response.data); + response.data = jsonDecode(deStr); + } catch (e) { + _logError("解密失败: $e"); + } + } + + handler.next(response); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + _logError("🔴 Error: ${err.message}"); + handler.next(err); + } + + // ============ 私有方法 ============ + + Future _addRequestHeaders(RequestOptions options) async { + // 开发模式添加 security: false + if (!kReleaseMode) { + options.headers['security'] = 'false'; + } + + // 注入 Token + final tokenManager = HelpToken(); + final token = tokenManager.get(); + // print('查看token: $token'); + if (token.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $token'; + } + + // 其他公共参数 + final deviceInfo = NyDeviceInfo.instance; + + options.headers.addAll({ + 'product-prefix': 'glimzo', + 'device-id': deviceInfo.deviceId, + 'system-type': deviceInfo.systemType, + 'model': deviceInfo.model, + 'system-version': deviceInfo.systemVersion, + 'brand': deviceInfo.brand, + 'app-version': deviceInfo.appVersion, + 'app-name': deviceInfo.appName, + 'lang-key': 'en', + 'time-zone': HelpTime.getTimeZone(), + 'idfa': '', + 'idfv': deviceInfo.idfv, + 'device-gaid': '', + }); + + // 处理 prevToken(从请求体中提取) + await _handleOldToken(options); + } + + Future _handleOldToken(RequestOptions options) async { + try { + if (options.data is Map) { + final body = options.data as Map; + final prevToken = body['prevToken']; + if (prevToken != null && prevToken.toString().isNotEmpty) { + options.headers['Authorization'] = prevToken.toString(); + } + } + } catch (_) { + // 非 JSON 请求跳过 + } + } + + void _logInfo(String message) { + if (enableLog) _logger.i(message); + } + + void _logDebug(String message) { + if (enableLog) _logger.d('💡 $message'); + } + + void _logError(String message) { + if (enableLog) _logger.e(message); + } + + void _handleError(dynamic e, bool error) { + _logError("🔴 Error: $e"); + // 这里可以根据需要处理错误提示 + } +} \ No newline at end of file diff --git a/lib/common/api/user_request.dart b/lib/common/api/user_request.dart new file mode 100644 index 0000000..67df43e --- /dev/null +++ b/lib/common/api/user_request.dart @@ -0,0 +1,41 @@ + +import 'package:novyronst/common/api/api_request.dart'; +import 'package:novyronst/common/api/api_string.dart'; +import 'package:novyronst/common/help/help_log.dart'; +import 'package:novyronst/common/model/novyronst_user.dart'; +import 'package:novyronst/common/model/register_model.dart'; + +class UserRequest { + + static Future login() { + final body = { + "avator": '', + "email": '123456@qq.com', + "family_name": 'test1', + "platform": 'android', + "third_id": '32434', + }; + return ApiRequest.post(ApiStringUser.login, data: body).then((value) { + HelpLog.print(value.data, tag: 'loginnnn'); + }); + } + + static Future register() { + return ApiRequest.post(ApiStringUser.register).then((value) { + if (value.isSuccess) { + return RegisterModel.fromJson(value.data['data']); + } + return null; + }); + } + + static Future getUserInfo() { + return ApiRequest.get(ApiStringUser.userInfo).then((value) { + if (value.isSuccess) { + HelpLog.print(value.data, tag: 'getUserInfo'); + return NovyronstUser.fromJson(value.data['data']); + } + return null; + }); + } +} \ No newline at end of file diff --git a/lib/common/api/video_request.dart b/lib/common/api/video_request.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/common/help/help_encrypt.dart b/lib/common/help/help_encrypt.dart new file mode 100644 index 0000000..986f9dd --- /dev/null +++ b/lib/common/help/help_encrypt.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:math'; + +class HelpEncrypt { + static const int bfSize = 2048; + static const String enStrTag = r'$'; + + // Generates a random salt of size between 16 and 64 bytes + static Uint8List randSalt() { + final rand = Random.secure(); + final size = rand.nextInt(49) + 16; // 16..64 + return Uint8List.fromList(List.generate(size, (_) => rand.nextInt(256))); + } + + // Encrypts the given data with a random salt + static Uint8List en(Uint8List data) { + if (data.isEmpty) return data; + final salt = randSalt(); + final encryptedData = enWithSalt(data, salt); + return Uint8List.fromList([salt.length, ...salt, ...encryptedData]); + } + + // Decrypts the given encrypted data + static Uint8List de(Uint8List data) { + if (data.isEmpty) return data; + final saltLen = data[0]; + final salt = data.sublist(1, 1 + saltLen); + final encrypted = data.sublist(1 + saltLen); + return deWithSalt(encrypted, salt); + } + + // Encrypts data with a specified salt + static Uint8List enWithSalt(Uint8List data, Uint8List salt) { + final mixedData = mixSalt(data, salt); + return cxEd(mixedData); + } + + // Decrypts data with a specified salt + static Uint8List deWithSalt(Uint8List data, Uint8List salt) { + final decryptedData = cxEd(data); + return removeSalt(decryptedData, salt); + } + + // Simple XOR-based encryption/decryption (bitwise negation) + static Uint8List cxEd(Uint8List data) { + return Uint8List.fromList(data.map((b) => b ^ 0xFF).toList()); + } + + // Apply salt to the data for encryption + static Uint8List mixSalt(Uint8List data, Uint8List salt) { + if (salt.isEmpty) return data; + return Uint8List.fromList( + List.generate(data.length, (i) { + final s = salt[i % salt.length]; + return calSalt(data[i], s); + }), + ); + } + + // Remove salt from the data after decryption + static Uint8List removeSalt(Uint8List data, Uint8List salt) { + if (salt.isEmpty) return data; + return Uint8List.fromList( + List.generate(data.length, (i) { + final s = salt[i % salt.length]; + return calRemoveSalt(data[i], s); + }), + ); + } + + // Calculate a modified value after mixing salt (for encryption) + static int calSalt(int v, int s) { + final r = v ^ 0xFF; + return s > r ? (s - r - 1) & 0xFF : (v + s) & 0xFF; + } + + // Reverse the salt mixing process for decryption + static int calRemoveSalt(int v, int s) { + return v >= s ? (v - s) & 0xFF : (0xFF - (s - v) + 1) & 0xFF; + } + + // Encrypt a string and return the hex representation + static String cxEStr(String data) { + return cxEStrAsBytes( + data, + ).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + } + + // Encrypt string and return as bytes + static Uint8List cxEStrAsBytes(String data) { + return cxEd(Uint8List.fromList(utf8.encode(data))); + } + + // Decrypt a hex string to a regular string + static String cxDStr(String data) { + final bytes = Uint8List.fromList([ + for (int i = 0; i < data.length; i += 2) + int.parse(data.substring(i, i + 2), radix: 16), + ]); + return utf8.decode(cxEd(bytes)); + } + + // Encrypt string data and return in the form of hex string with prefix + static String enStr(String data) { + return enBytesStr(Uint8List.fromList(utf8.encode(data))); + } + + // Encrypt bytes and return as hex string with prefix + static String enBytesStr(Uint8List data) { + final encrypted = en(data); + return '$enStrTag${encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}'; + } + + // Decrypt a string with encoded prefix (e.g., "$" symbol) + static String deStr(String data) { + return utf8.decode(deStrBytes(data)); + } + + // Decrypt the byte array extracted from a string + static Uint8List deStrBytes(String data) { + if (!data.startsWith(enStrTag)) { + throw ArgumentError("Invalid encoded string"); + } + final hexData = data.substring(1); + final bytes = Uint8List.fromList([ + for (int i = 0; i < hexData.length; i += 2) + int.parse(hexData.substring(i, i + 2), radix: 16), + ]); + return de(bytes); + } + + // Copy encrypted data from a stream to another stream + static Future copy( + Stream> reader, + StreamSink> writer, + ) async { + int total = 0; + await for (final chunk in reader) { + total += chunk.length; + final encrypted = chunk.map((b) => b ^ 0xFF).toList(); + writer.add(encrypted); + } + await writer.close(); + return total; + } + + // Custom encryption with condition (even and odd byte values handled differently) + static Uint8List cxEd1(Uint8List data) { + return Uint8List.fromList( + data.map((b) => (b % 2 == 1) ? (b ^ 0xFF) : b).toList(), + ); + } +} diff --git a/lib/common/help/help_image.dart b/lib/common/help/help_image.dart index 7efcc2e..78b034d 100644 --- a/lib/common/help/help_image.dart +++ b/lib/common/help/help_image.dart @@ -45,7 +45,9 @@ class ConstImage { static const String settings = "assets/other/settings.png"; // copy static const String copy = "assets/other/copy.png"; - + // pricary web_site + static const String primary = "assets/other/pricary.png"; + static const String webSite = "assets/other/web_site.png"; } diff --git a/lib/common/help/help_log.dart b/lib/common/help/help_log.dart new file mode 100644 index 0000000..5629f1e --- /dev/null +++ b/lib/common/help/help_log.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:talker_flutter/talker_flutter.dart'; + +class HelpLog { + HelpLog._(); + + static final HelpLog _instance = HelpLog._(); + + static HelpLog get instance => _instance; + + final Talker _talker = TalkerFlutter.init(); + + factory HelpLog() => _instance; + + Talker get tLog => _talker; + + /// DevTools / 控制台里可按名称过滤。 + static const String logName = 'HelpLog'; + + Future initLogger() async { + /// app 信息 + } + + /// 仅 [kDebugMode] 输出;内容经安全格式化,不会因无法 JSON 编码而中断业务逻辑。 + static void print( + Object? message, { + String? tag, + String name = logName, + }) { + if (!kDebugMode) return; + try { + final text = _stringify(message); + final line = tag != null ? '[debug-tag: $tag] ===== $text' : text; + log(line, name: name); + } catch (e, st) { + log( + '[SkyLog.print failed] $e', + name: name, + error: e, + stackTrace: st, + ); + } + } + + /// 尽量输出缩进 JSON;无法编码时退回 [toString],不向外抛错。 + static String _stringify(Object? value) { + if (value == null) return 'null'; + if (value is String) return value; + if (value is num || value is bool) return value.toString(); + + const encoder = JsonEncoder.withIndent(' '); + + if (value is Map) { + try { + return encoder.convert(value); + } catch (_) { + return value.toString(); + } + } + if (value is List) { + try { + return encoder.convert(value); + } catch (_) { + return value.toString(); + } + } + if (value is Iterable) { + try { + return encoder.convert(value.toList()); + } catch (_) { + return value.toString(); + } + } + + try { + return encoder.convert(value); + } catch (_) { + try { + return '${value.runtimeType}: $value'; + } catch (_) { + return ''; + } + } + } + + /// 错误日志走 Talker;若 Talker 抛错,在 Debug 下用 [log] 兜底,仍不向外抛。 + static void error( + Object? message, { + String? tag, + StackTrace? stackTrace, + }) { + final text = message == null ? 'null' : '$message'; + try { + _instance._talker.logCustom( + CustomLog( + text, + settings: _instance._talker.settings, + tag: tag, + logLevel: LogLevel.error, + stackTrace: stackTrace, + ), + ); + } catch (e, st) { + if (kDebugMode) { + log( + '[SkyLog.error fallback] $text | $e', + name: logName, + error: e, + stackTrace: st, + ); + } + } + } +} + +class CustomLog extends TalkerLog { + final String? tag; + final TalkerSettings settings; + final LogLevel? _logLevel; + final StackTrace? _stackTrace; + + CustomLog( + super.message, { + this.tag, + required this.settings, + LogLevel logLevel = LogLevel.debug, + StackTrace? stackTrace, + }) : _logLevel = logLevel, + _stackTrace = stackTrace; + + @override + String? get key => tag; + + @override + LogLevel? get logLevel => _logLevel; + + @override + StackTrace? get stackTrace => _stackTrace; + + @override + AnsiPen? get pen => + settings.colors[TalkerKey.fromLogLevel(logLevel ?? LogLevel.info)]; +} diff --git a/lib/common/help/help_time.dart b/lib/common/help/help_time.dart new file mode 100644 index 0000000..13b67f2 --- /dev/null +++ b/lib/common/help/help_time.dart @@ -0,0 +1,55 @@ + +class HelpTime { + + static String getTimeZone() { + final offset = DateTime.now().timeZoneOffset; + final hours = offset.inHours; + final minutes = offset.inMinutes.remainder(60).abs(); + final sign = hours >= 0 ? '+' : '-'; + final formatted = + 'GMT$sign${hours.abs().toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}'; + return formatted; + } + + //格式化数字,带单位 k / m,可自定义小数位和千分位分隔符 + static String formatNum( + num value, { + int decimalDigits = 1, + String separator = ',', + String unit = 'k', + }) { + String removeTrailingZeros(String str) { + if (str.contains('.')) { + str = str.replaceAll(RegExp(r'0+$'), ''); // 去掉末尾 0 + str = str.replaceAll(RegExp(r'\.$'), ''); // 去掉末尾小数点 + } + return str; + } + + String addSeparator(String str) { + final parts = str.split('.'); + final intPart = parts[0]; + final decPart = parts.length > 1 ? '.${parts[1]}' : ''; + final regex = RegExp(r'(\d+)(\d{3})'); + var formatted = intPart; + while (regex.hasMatch(formatted)) { + formatted = formatted.replaceAllMapped( + regex, + (m) => '${m[1]}$separator${m[2]}', + ); + } + return '$formatted$decPart'; + } + + if (value < 1000) { + return addSeparator(value.toStringAsFixed(0)); + } else if (value < 1000000) { + int mu = unit=='w' ? 10000 : 1000; + final formatted = (value / mu).toStringAsFixed(decimalDigits); + return '${removeTrailingZeros(formatted)}$unit'; + } else { + final formatted = (value / 1000000).toStringAsFixed(decimalDigits); + return '${removeTrailingZeros(formatted)}m'; + } + } +} \ No newline at end of file diff --git a/lib/common/help/help_token.dart b/lib/common/help/help_token.dart new file mode 100644 index 0000000..56315b5 --- /dev/null +++ b/lib/common/help/help_token.dart @@ -0,0 +1,228 @@ + +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:novyronst/common/api/user_request.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class HelpToken { + static final HelpToken _instance = HelpToken._internal(); + factory HelpToken() => _instance; + HelpToken._internal(); + + static const String _keyToken = 'SKY_STORE_TOKEN'; + static const String _keyUserInfo = 'SKY_STORE_USER_INFO'; + + String? _cachedToken; + Map? _cachedUserInfo; + + + /// 获取 Token(从内存或存储) + String get() { + if (_cachedToken != null) { + return _cachedToken!; + } + // 如果内存中没有,从存储中加载 + _cachedToken = _getFromStorage(_keyToken); + return _cachedToken ?? ''; + } + + /// 保存 Token(到内存和存储) + void save(String token) { + if (token.isNotEmpty) { + _cachedToken = token; + _saveToStorage(_keyToken, token); + } + } + + /// 清除 Token(从内存和存储) + void clear() { + _cachedToken = null; + _removeFromStorage(_keyToken); + _cachedUserInfo = null; + _removeFromStorage(_keyUserInfo); + } + + /// 刷新 Token(调用注册接口获取新 Token) + /// 注意:这里需要传入一个刷新函数,因为原项目依赖 GlimzoRequestUser + Future update() async { + try { + final res = await UserRequest.register(); + if (res == null) return; + final newToken = res.token; + if (newToken.isNotEmpty ?? false) { + // 保存新 Token + save(newToken); + // 如果返回了用户信息,也一并保存 + // if (res['userInfo'] != null) { + // saveUserInfo(res['userInfo']); + // } + } else { + throw Exception('Invalid token response'); + } + } catch (e) { + debugPrint('[TokenManager] Error updating token: $e'); + rethrow; + } + } + + // ============ 便捷属性 ============ + + /// 判断 Token 是否为空 + bool get isEmpty => get().isEmpty; + + /// 判断 Token 是否不为空 + bool get isNotEmpty => !isEmpty; + + /// 获取完整 Authorization 头 + String? get authorizationHeader { + final token = get(); + if (token.isNotEmpty) { + return 'Bearer $token'; + } + return null; + } + + // ============ 用户信息扩展(原项目没有,但建议加上) ============ + + /// 保存用户信息 + void saveUserInfo(Map userInfo) { + _cachedUserInfo = userInfo; + _saveToStorage(_keyUserInfo, jsonEncode(userInfo)); + } + + /// 获取用户信息 + Map? getUserInfo() { + if (_cachedUserInfo != null) { + return _cachedUserInfo; + } + final stored = _getFromStorage(_keyUserInfo); + if (stored != null && stored.isNotEmpty) { + try { + _cachedUserInfo = jsonDecode(stored) as Map; + return _cachedUserInfo; + } catch (_) { + return null; + } + } + return null; + } + + // ============ 私有存储方法 ============ + + String? _getFromStorage(String key) { + try { + // 使用 SharedPreferences 同步获取(但 SharedPreferences 是异步的) + // 为了保持与原项目一致的同步接口,这里使用异步转同步 + // 但更好的方式是在初始化时预加载 + return _getFromStorageSync(key); + } catch (e) { + debugPrint('[TokenManager] Error reading from storage: $e'); + return null; + } + } + + // 同步读取(注意:SharedPreferences 本身是异步的,这里用同步方式需要谨慎) + String? _getFromStorageSync(String key) { + // 由于 SharedPreferences 是异步的,我们使用一个全局缓存 + // 实际项目中建议在 main 中调用 init() 预加载 + try { + // 这里为了简化,直接返回 null,实际应该用异步方式 + // 建议使用下面的异步版本 + return null; + } catch (_) { + return null; + } + } + + void _saveToStorage(String key, String value) { + // 异步保存,不等待结果 + _saveToStorageAsync(key, value); + } + + void _removeFromStorage(String key) { + // 异步移除,不等待结果 + _removeFromStorageAsync(key); + } + + // ============ 异步存储方法(推荐使用) ============ + + /// 初始化:从存储中加载 Token + Future init() async { + try { + final prefs = await SharedPreferences.getInstance(); + _cachedToken = prefs.getString(_keyToken); + + final userInfoStr = prefs.getString(_keyUserInfo); + if (userInfoStr != null && userInfoStr.isNotEmpty) { + try { + _cachedUserInfo = jsonDecode(userInfoStr) as Map; + } catch (_) { + _cachedUserInfo = null; + } + } + } catch (e) { + debugPrint('[TokenManager] Init error: $e'); + } + } + + /// 异步获取 Token + Future getAsync() async { + if (_cachedToken != null) { + return _cachedToken!; + } + try { + final prefs = await SharedPreferences.getInstance(); + _cachedToken = prefs.getString(_keyToken); + return _cachedToken ?? ''; + } catch (e) { + debugPrint('[TokenManager] Get token error: $e'); + return ''; + } + } + + /// 异步保存 Token + Future saveAsync(String token) async { + if (token.isNotEmpty) { + _cachedToken = token; + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_keyToken, token); + } catch (e) { + debugPrint('[TokenManager] Save token error: $e'); + } + } + } + + /// 异步清除 Token + Future clearAsync() async { + _cachedToken = null; + _cachedUserInfo = null; + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_keyToken); + await prefs.remove(_keyUserInfo); + } catch (e) { + debugPrint('[TokenManager] Clear token error: $e'); + } + } + + // 异步保存到存储 + Future _saveToStorageAsync(String key, String value) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(key, value); + } catch (e) { + debugPrint('[TokenManager] Save to storage error: $e'); + } + } + + // 异步从存储移除 + Future _removeFromStorageAsync(String key) async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(key); + } catch (e) { + debugPrint('[TokenManager] Remove from storage error: $e'); + } + } +} \ No newline at end of file diff --git a/lib/common/model/novyronst_user.dart b/lib/common/model/novyronst_user.dart new file mode 100644 index 0000000..e756ce6 --- /dev/null +++ b/lib/common/model/novyronst_user.dart @@ -0,0 +1,60 @@ +import 'package:novyronst/common/model/parse_model.dart'; + + +class NovyronstUser { + final String customerId; + final bool isTourist; + final String userLevel; + final String avator; + final String? familyName; + final String? givingName; + + const NovyronstUser({ + required this.customerId, + required this.isTourist, + required this.userLevel, + required this.avator, + this.familyName, + this.givingName, + }); + + factory NovyronstUser.fromJson(Map json) { + return NovyronstUser( + customerId: parseString(json['customer_id']), + isTourist: parseBool(json['is_tourist']), + userLevel: parseString(json['user_level']), + avator: parseString(json['avator']), + familyName: json['family_name'] as String?, + givingName: parseString(json['giving_name']), + ); + } + + Map toJson() { + return { + 'customer_id': customerId, + 'is_tourist': isTourist, + 'user_level': userLevel, + 'avator': avator, + 'family_name': familyName, + 'giving_name': givingName, + }; + } + + NovyronstUser copyWith({ + String? customerId, + bool? isTourist, + String? userLevel, + String? avator, + String? familyName, + String? givingName, + }) { + return NovyronstUser( + customerId: customerId ?? this.customerId, + isTourist: isTourist ?? this.isTourist, + userLevel: userLevel ?? this.userLevel, + avator: avator ?? this.avator, + familyName: familyName ?? this.familyName, + givingName: givingName ?? this.givingName, + ); + } +} diff --git a/lib/common/model/parse_model.dart b/lib/common/model/parse_model.dart new file mode 100644 index 0000000..9ef5217 --- /dev/null +++ b/lib/common/model/parse_model.dart @@ -0,0 +1,57 @@ +bool parseBool(dynamic value) { + if (value == null) return false; + if (value is bool) return value; + if (value is num) return value != 0; + if (value is String) { + final v = value.trim().toLowerCase(); + return v == 'true' || v == '1' || v == 'yes' || v == 'y'; + } + return false; +} + +int parseInt(dynamic value) { + if (value == null) return 0; + if (value is int) return value; + if (value is double) return value.toInt(); + if (value is num) return value.toInt(); + if (value is String) return int.tryParse(value) ?? 0; + return 0; +} + +String parseString(dynamic value) { + if (value == null) return ''; + return value.toString(); +} + +List? parseStringList(dynamic value) { + if (value == null) return null; + if (value is List) { + return value.map((item) => parseString(item)).toList(); + } + return null; +} + +List? parseIntList(dynamic value) { + if (value == null) return null; + if (value is List) { + return value.map((item) => parseInt(item)).toList(); + } + return null; +} + +Map? parseMap(dynamic value) { + if (value is Map) return value; + if (value is Map) return Map.from(value); + return null; +} + +List parseModelList( + dynamic value, + T Function(Map json) fromJson, + ) { + if (value is! List) return []; + return value + .whereType() + .map((item) => fromJson(Map.from(item))) + .toList(); +} diff --git a/lib/common/model/register_model.dart b/lib/common/model/register_model.dart new file mode 100644 index 0000000..a5edb30 --- /dev/null +++ b/lib/common/model/register_model.dart @@ -0,0 +1,18 @@ + +class RegisterModel { + final String token; + final int customerId; + final bool? autoLogin; + final int touristId; + + RegisterModel({required this.token, required this.customerId, this.autoLogin, required this.touristId}); + + factory RegisterModel.fromJson(Map json) { + return RegisterModel( + token: json['token'], + customerId: json['customer_id'], + autoLogin: json['auto_login'], + touristId: json['tourist_id'], + ); + } +} \ No newline at end of file diff --git a/lib/pages/my/pages/my_page.dart b/lib/pages/my/pages/my_page.dart index 14b75df..255b0eb 100644 --- a/lib/pages/my/pages/my_page.dart +++ b/lib/pages/my/pages/my_page.dart @@ -3,6 +3,7 @@ import 'package:novyronst/common/help/help_image.dart'; import 'package:novyronst/pages/my/widgets/ny_dash_line.dart'; import 'package:novyronst/root/lib_export.dart'; import 'package:novyronst/tools/widgets/app_bg_page.dart'; +import 'package:novyronst/tools/widgets/extend_widget.dart'; import 'package:novyronst/tools/widgets/novy_com_widget.dart'; class MyPage extends StatefulWidget { @@ -16,6 +17,10 @@ class MyPage extends StatefulWidget { class _RootPageState extends State { + List _titles = [ + 'History', 'Settings', 'About', 'Privacy Policy', 'User Agreement' + ]; + @override Widget build(BuildContext context) { return AppBgPage( @@ -26,13 +31,65 @@ class _RootPageState extends State { children: [ SizedBox(height: AppScreen.statusBarHeight + 48), _buildUserInfo(), - NyDashLine() + const SizedBox(height: 26), + ...List.generate(_titles.length, (index) { + return _buildSectionView(_titles[index], index, onTap: () { + if (index == 0) { + + } else if (index == 1) { + + } else if (index == 2) { + + } else if (index == 3) { + + } else if (index == 4) { + + } + }); + }) ], ), ), ); } + Widget _buildSectionView(String title, int index, {Function()? onTap}) { + String imgName = ConstImage.history; + if (index == 1) { + imgName = ConstImage.settings; + } else if (index == 2) { + imgName = ConstImage.about; + } else if (index == 3) { + imgName = ConstImage.primary; + } else if (index == 4) { + imgName = ConstImage.webSite; + } + return Container( + height: 60, + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + Expanded(child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + HelpImage.asset(imgName, width: 36, height: 36), + const SizedBox(width: 10), + NovyText(title, + fontSize: 14, + ) + ], + ), + Icon(Icons.arrow_forward_ios, size: 17, color: Colors.white) + ], + )), + if (index < 4) NyDashLine() + ], + ), + ).addInkWell(onTap: onTap); + } + Widget _buildUserInfo() { return Row( children: [ diff --git a/lib/tools/util/ny_device_info.dart b/lib/tools/util/ny_device_info.dart new file mode 100644 index 0000000..3be279b --- /dev/null +++ b/lib/tools/util/ny_device_info.dart @@ -0,0 +1,177 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:android_id/android_id.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:novyronst/common/api/api_string.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +@immutable +class NyDeviceInfo { + final String deviceId; + final String systemType; + final String systemVersion; + final String brand; + final String model; + + final String appName; + final String appPackageName; + final String appVersion; + final String idfv; + + static NyDeviceInfo? _instance; + + const NyDeviceInfo._({ + required this.deviceId, + required this.systemType, + required this.systemVersion, + required this.brand, + required this.model, + required this.appName, + required this.appPackageName, + required this.appVersion, + required this.idfv, + }); + + static NyDeviceInfo get instance { + if (_instance == null) { + throw StateError('error'); + } + return _instance!; + } + + @visibleForTesting + static void setMockInstance({ + String deviceId = 'test-device-id', + String systemType = 'test', + String systemVersion = '1.0', + String brand = 'test', + String model = 'test', + String appName = 'Glimzo', + String appPackageName = 'com.example.skywood', + String appVersion = '1.0.0', + String idfv = 'test-idfv', + }) { + _instance = NyDeviceInfo._( + deviceId: deviceId, + systemType: systemType, + systemVersion: systemVersion, + brand: brand, + model: model, + appName: appName, + appPackageName: appPackageName, + appVersion: appVersion, + idfv: idfv, + ); + } + + /// 初始化设备信息 + static Future init() async { + if (_instance != null) return; + + String deviceId = '', + systemType = '', + systemVersion = '', + brand = '', + model = '', + appName = 'Glimzo', + appPackageName = '', + appVersion = '', + idfv = ''; + + // 获取应用信息 + await _initAppInfo() + .then((info) { + appPackageName = info['packageName'] ?? ''; + appVersion = info['version'] ?? ''; + appName = ApiString.appName; + }) + .catchError((e) { + debugPrint('[sDeviceInfo] getElPackageName: $e'); + }); + + // 获取设备信息 + if (Platform.isAndroid) { + await _initAndroidInfo() + .then((info) { + deviceId = info['deviceId'] ?? ''; + systemType = 'android'; + systemVersion = info['systemVersion'] ?? ''; + brand = info['brand'] ?? ''; + model = info['model'] ?? ''; + }) + .catchError((e) { + debugPrint('[GlimzoUtilDeviceInfo] Android: $e'); + }); + } else if (Platform.isIOS) { + await _initIOSInfo() + .then((info) { + deviceId = info['deviceId'] ?? ''; + idfv = info['idfv'] ?? ''; + systemType = 'ios'; + systemVersion = info['systemVersion'] ?? ''; + brand = info['brand'] ?? ''; + model = info['model'] ?? ''; + }) + .catchError((e) { + debugPrint('[GlimzoUtilDeviceInfo] iOS: $e'); + }); + } + + _instance = NyDeviceInfo._( + deviceId: deviceId, + systemType: systemType, + systemVersion: systemVersion, + brand: brand, + model: model, + appName: appName, + appPackageName: appPackageName, + appVersion: appVersion, + idfv: idfv, + ); + } + + /// 获取应用信息 + static Future> _initAppInfo() async { + final packageInfo = await PackageInfo.fromPlatform(); + return { + 'packageName': packageInfo.packageName, + 'version': packageInfo.version, + }; + } + + /// 获取 Android 设备信息 + static Future> _initAndroidInfo() async { + final plugin = DeviceInfoPlugin(); + final androidInfo = await plugin.androidInfo; + final androidIdPlugin = AndroidId(); + final androidId = await androidIdPlugin.getId(); + return { + 'deviceId': androidId ?? androidInfo.id, + 'systemVersion': androidInfo.version.release, + 'brand': androidInfo.brand, + 'model': androidInfo.model, + }; + } + + /// 获取 iOS 设备信息 + static Future> _initIOSInfo() async { + final plugin = DeviceInfoPlugin(); + final iosInfo = await plugin.iosInfo; + final secureStorage = FlutterSecureStorage(); + String? idfvStore = await secureStorage.read(key: 'identifierForVendor'); + idfvStore ??= iosInfo.identifierForVendor; + + if (idfvStore != null) { + await secureStorage.write(key: 'identifierForVendor', value: idfvStore); + } + + return { + 'deviceId': idfvStore, + 'idfv': iosInfo.identifierForVendor ?? '', + 'systemVersion': iosInfo.systemVersion, + 'brand': iosInfo.model, + 'model': iosInfo.modelName, + }; + } +} diff --git a/lib/tools/widgets/app_toast_util.dart b/lib/tools/widgets/app_toast_util.dart new file mode 100644 index 0000000..a4a7d45 --- /dev/null +++ b/lib/tools/widgets/app_toast_util.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:novyronst/common/help/help_image.dart'; + +// 加载数据的函数类型 +typedef LoadBlock = Future Function(); + +// 弹出Toast提示的类 +class AppToastUtil { + static const String _defaultLoadingText = ""; + static const String _loadingText = "loading..."; + + /// 显示普通 Toast 提示 + static Future showToast( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) async { + await dismiss(); + return SmartDialog.showToast( + msg.toString(), + alignment: Alignment.center, + debounce: true, + displayTime: displayTime, + ); + } + + static Future showError( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) { + return showFail(msg, displayTime: displayTime); + } + + /// 显示失败提示 + static Future showFail( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + Function()? onDismiss, + }) async { + await dismiss(); + return SmartDialog.showNotify( + msg: msg.toString(), + alignment: Alignment.center, + notifyType: NotifyType.failure, + displayTime: displayTime, + debounce: true, + onDismiss: onDismiss, + ); + } + + /// 显示成功提示 + static Future showSuccess( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) async { + await dismiss(); + return SmartDialog.showNotify( + msg: msg.toString(), + notifyType: NotifyType.success, + debounce: true, + displayTime: displayTime, + ); + } + + static Future showInfo( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) { + return showWarning(msg, displayTime: displayTime); + } + + static Future showWarning( + Object msg, { + Duration displayTime = const Duration(seconds: 2), + }) async { + await dismiss(); + return SmartDialog.showNotify( + msg: msg.toString(), + notifyType: NotifyType.warning, + debounce: true, + displayTime: displayTime, + ); + } + + /// 显示 Lottie 加载提示 + static Future showLottieLoading({ + Duration? displayTime, + }) async { + await dismiss(); + return SmartDialog.showLoading( + displayTime: displayTime ?? const Duration(seconds: 5), + maskColor: Colors.transparent, + builder: (_) { + return SizedBox( + width: 120, + height: 120, + // child: Image.asset(ConstImage.loading, fit: BoxFit.fill), + ); + }, + ); + } + + /// 显示加载提示 + static Future showLoading({ + String text = _loadingText, + Duration displayTime = const Duration(seconds: 10), + }) async { + await dismiss(); + return SmartDialog.showLoading(msg: text, displayTime: displayTime); + } + + /// 关闭特定状态的弹窗 + static Future dismiss({SmartStatus status = SmartStatus.loading}) { + return SmartDialog.dismiss(status: status); + } + + /// 关闭所有弹窗(占位) + static void dismissAll() { + SmartDialog.dismiss(); + } + + /// 自动显示加载框并在完成后关闭 + static Future loading( + LoadBlock block, { + String? text, + bool isLoading = true, + }) async { + if (isLoading) { + showLoading(text: text ?? _defaultLoadingText); + } + + try { + final result = await block(); + return result; + } finally { + dismiss(); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 50c91a1..50a9a88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" args: dependency: transitive description: @@ -65,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" crypto: dependency: transitive description: @@ -137,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -240,6 +264,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + group_button: + dependency: transitive + description: + name: group_button + sha256: "0610fcf28ed122bfb4b410fce161a390f7f2531d55d1d65c5375982001415940" + url: "https://pub.dev" + source: hosted + version: "5.3.4" hooks: dependency: transitive description: @@ -320,6 +352,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" logging: dependency: transitive description: @@ -480,6 +520,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" + url: "https://pub.dev" + source: hosted + version: "12.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: @@ -573,6 +629,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + talker: + dependency: transitive + description: + name: talker + sha256: f1a14d623f1d1bec42bb3bb77674eb766ffe8d26e5f79af652d85cb097c3e757 + url: "https://pub.dev" + source: hosted + version: "5.1.17" + talker_flutter: + dependency: "direct dev" + description: + name: talker_flutter + sha256: "54cbbf852101721664faf4a05639fd2fdefdc37178327990abea00390690d4bc" + url: "https://pub.dev" + source: hosted + version: "5.1.16" + talker_logger: + dependency: transitive + description: + name: talker_logger + sha256: "459205c3e571f97ecc6be6e1b1b7e6b97b853e78ea458894650be407596e3216" + url: "https://pub.dev" + source: hosted + version: "5.1.17" term_glyph: dependency: transitive description: @@ -597,6 +677,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 26dac61..95aa29e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: flutter_smart_dialog: ^5.1.0 flutter_secure_storage: ^9.2.4 android_id: ^0.5.1 + logger: ^2.5.0 @@ -36,8 +37,8 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^6.0.0 + talker_flutter: ^5.0.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec