diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9f1113c..4507522 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true" + android:networkSecurityConfig="@xml/network_security_config"> + - - + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..da41a20 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + api.jjsos.cn + api.test.jjsos.cn + pic.jjsos.cn + + diff --git a/lib/api/_api_client.dart b/lib/api/_api_client.dart new file mode 100644 index 0000000..40fd3a2 --- /dev/null +++ b/lib/api/_api_client.dart @@ -0,0 +1,24 @@ +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:autosos_flutter/util/xhttp.dart'; + +/// 提供已注入 token 的 XHttp(保证所有业务 API 自动带 token) +class ApiClient { + ApiClient._(); + static final ApiClient instance = ApiClient._(); + + XHttp get http => XHttp.getInstance(); + + /// 登录态写入后调用,刷新后续请求头 + void setToken(String token) { + XHttp.setHeader('Authorization', 'Bearer $token'); + } + + void clearToken() { + XHttp.removeHeader('Authorization'); + } + + String? get token { + final v = SPUtil().get('accessToken'); + return v is String ? v : null; + } +} \ No newline at end of file diff --git a/lib/api/login_api.dart b/lib/api/login_api.dart index 91ee81e..32b0410 100644 --- a/lib/api/login_api.dart +++ b/lib/api/login_api.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:autosos_flutter/pages/home/home_page.dart'; import 'package:autosos_flutter/util/sp_util.dart'; import 'package:autosos_flutter/util/xhttp.dart'; diff --git a/lib/api/order_api.dart b/lib/api/order_api.dart new file mode 100644 index 0000000..81f5ca2 --- /dev/null +++ b/lib/api/order_api.dart @@ -0,0 +1,188 @@ +import 'package:autosos_flutter/api/_api_client.dart'; +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/misc.dart'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/model/payment.dart'; + +/// 订单 API(对应 Android AppApiService 中 order/* 相关) +class OrderApi { + static final OrderApi instance = OrderApi._(); + OrderApi._(); + + final _http = ApiClient.instance.http; + + // ===== 核单 ===== + + /// 核单列表 + Future>> checkList({int page = 1, int size = 20}) async { + final r = await _http.get( + '/v2/order/check-list', + {'page': page, 'size': size}, + ); + return BaseResponse.fromJson(r as Map, + (d) => (d as List).map((e) => CheckItem.fromJson(e as Map)).toList()); + } + + /// 提交核单 + Future> submitCheck({ + required int orderId, + required String remark, + }) async { + final r = await _http.post('/v2/order/check', { + 'order_id': orderId, + 'remark': remark, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + // ===== 等待接单 ===== + + /// 当前可接订单列表 + Future>> waitingList() async { + final r = await _http.get('/v2/order/waiting-list'); + return BaseResponse.fromJson(r as Map, + (d) => (d as List).map((e) => WaitingOrder.fromJson(e as Map)).toList()); + } + + /// 抢单 + Future> grabOrder(int orderId) async { + final r = await _http.post('/v2/order/grab', {'order_id': orderId}); + return BaseResponse.fromJson(r as Map, + (d) => Order.fromJson(d as Map)); + } + + // ===== 救援中 ===== + + /// 到达现场 + Future> arriveScene(int orderId, {double? lat, double? lng}) async { + final r = await _http.post('/v2/order/arrive', { + 'order_id': orderId, + 'lat': lat, + 'lng': lng, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 开始轨迹录制 + Future> startTrack(int orderId) async { + final r = await _http.post('/v2/order/track/start', {'order_id': orderId}); + return BaseResponse.fromJson(r as Map, + (d) => d as String); + } + + /// 上传轨迹点 + Future> uploadTrackPoints(int orderId, List> points) async { + final r = await _http.post('/v2/order/track/points', { + 'order_id': orderId, + 'points': points, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 结束轨迹 + Future> endTrack(int orderId) async { + final r = await _http.post('/v2/order/track/end', {'order_id': orderId}); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + // ===== 拍照取证 ===== + + /// 上传照片(实际通过 QiniuUploadService 走七牛,本接口只是返回 token) + Future>> uploadPhotos(int orderId, List localPaths) async { + final r = await _http.post('/v2/order/photos', { + 'order_id': orderId, + 'paths': localPaths, + }); + return BaseResponse.fromJson(r as Map, + (d) => (d as List).cast()); + } + + // ===== 客户签字 ===== + + /// 上传签名图片 + Future> uploadSignature(int orderId, String localPath) async { + final r = await _http.post('/v2/order/signature', { + 'order_id': orderId, + 'path': localPath, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as String); + } + + // ===== 收款 ===== + + /// 生成支付订单(微信支付) + Future> createWxPay(int orderId, double amount) async { + final r = await _http.post('/v2/pay/wx', { + 'order_id': orderId, + 'amount': amount, + }); + return BaseResponse.fromJson(r as Map, + (d) => WxPay.fromJson(d as Map)); + } + + /// 生成收款二维码(技师二维码让客户扫码) + Future> createPayQrCode(int orderId, double amount) async { + final r = await _http.post('/v2/pay/qrcode', { + 'order_id': orderId, + 'amount': amount, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as String); + } + + /// 现金收款确认 + Future> confirmCashPay(int orderId, double amount) async { + final r = await _http.post('/v2/pay/cash', { + 'order_id': orderId, + 'amount': amount, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + // ===== 完成 ===== + + /// 提交完成(带金额明细) + Future> completeOrder({ + required int orderId, + required List amountItems, + String? remark, + }) async { + final r = await _http.post('/v2/order/complete', { + 'order_id': orderId, + 'amount_items': amountItems.map((e) => e.toJson()).toList(), + 'remark': remark, + }); + return BaseResponse.fromJson(r as Map, + (d) => CompleteOrderInfo.fromJson(d as Map)); + } + + // ===== 订单记录 ===== + + /// 历史订单(已完成 / 已取消) + Future>> historyOrders({ + int page = 1, + int size = 20, + String? status, + }) async { + final r = await _http.get('/v2/order/history', { + 'page': page, + 'size': size, + if (status != null) 'status': status, + }); + return BaseResponse.fromJson(r as Map, + (d) => PageResult.fromJson(d as Map, (e) => Order.fromJson(e as Map))); + } + + /// 订单详情 + Future> orderDetail(int orderId) async { + final r = await _http.get('/v2/order/detail/$orderId'); + return BaseResponse.fromJson(r as Map, + (d) => Order.fromJson(d as Map)); + } +} diff --git a/lib/api/upload_api.dart b/lib/api/upload_api.dart new file mode 100644 index 0000000..7e48696 --- /dev/null +++ b/lib/api/upload_api.dart @@ -0,0 +1,29 @@ +import 'package:autosos_flutter/api/_api_client.dart'; +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/misc.dart'; +import 'package:autosos_flutter/service/upload_service.dart'; + +/// 文件上传 API(七牛凭证获取) +class UploadApi { + static final UploadApi instance = UploadApi._(); + UploadApi._(); + + final _http = ApiClient.instance.http; + + /// 获取七牛上传凭证 + Future> qiNiuToken() async { + final r = await _http.get('/v2/upload/qiniu-token'); + return BaseResponse.fromJson(r as Map, + (d) => QiNiuToken.fromJson(d as Map)); + } + + /// 通过封装的上传服务上传文件,返回远程 URL + Future uploadFile(String localPath) async { + return UploadService.instance.upload(localPath); + } + + /// 批量上传 + Future> uploadFiles(List localPaths) async { + return UploadService.instance.uploadBatch(localPaths); + } +} \ No newline at end of file diff --git a/lib/api/user_api.dart b/lib/api/user_api.dart new file mode 100644 index 0000000..498dcce --- /dev/null +++ b/lib/api/user_api.dart @@ -0,0 +1,308 @@ +import 'package:autosos_flutter/api/_api_client.dart'; +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/misc.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/model/user.dart'; + +/// 用户 / 认证 / 我的 相关 API +class UserApi { + static final UserApi instance = UserApi._(); + UserApi._(); + + final _http = ApiClient.instance.http; + + // ===== 登录相关 ===== + + /// 账号密码登录 + Future> login({ + required String username, + required String password, + String? code, + String? cid, + String? wxOpenid, + String? wxUnionid, + }) async { + final r = await _http.post('/v2/auth/get-access-token', { + 'type': 1, + 'username': username, + 'password': password, + 'code': code ?? '', + 'getui_cid': cid ?? '', + 'wx_openid': wxOpenid ?? '', + 'wx_unionid': wxUnionid ?? '', + }); + return BaseResponse.fromJson(r as Map, + (d) => LoginToken.fromJson(d as Map)); + } + + /// 发送短信验证码 + Future> sendSms(String mobile) async { + final r = await _http.post('/v2/auth/send-sms', {'mobile': mobile}); + return BaseResponse.fromJson(r as Map, + (d) => SmsResult.fromJson(d as Map)); + } + + /// 手机验证码登录 + Future> loginBySms({ + required String mobile, + required String code, + String? cid, + }) async { + final r = await _http.post('/v2/auth/login-by-sms', { + 'mobile': mobile, + 'code': code, + 'cid': cid, + }); + return BaseResponse.fromJson(r as Map, + (d) => LoginToken.fromJson(d as Map)); + } + + /// 微信登录(前端通过 fluwx 拿到 code,调后端) + Future> loginByWx({ + required String wxCode, + String? cid, + }) async { + final r = await _http.post('/v2/auth/login-by-wx', { + 'wx_code': wxCode, + 'cid': cid, + }); + return BaseResponse.fromJson(r as Map, + (d) => LoginToken.fromJson(d as Map)); + } + + /// 绑定手机号 + Future> bindPhone({ + required String mobile, + required String code, + }) async { + final r = await _http.post('/v2/auth/bind-phone', { + 'mobile': mobile, + 'code': code, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 绑定微信 + Future> bindWx({ + required String wxCode, + required String mobile, + required String code, + }) async { + final r = await _http.post('/v2/auth/bind-wx', { + 'wx_code': wxCode, + 'mobile': mobile, + 'code': code, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 退出登录 + Future> logout() async { + final r = await _http.post('/v2/auth/logout'); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + // ===== 我的 ===== + + /// 用户信息 + Future> info() async { + final r = await _http.get('/v2/user/info'); + return BaseResponse.fromJson(r as Map, + (d) => UserInfo.fromJson(d as Map)); + } + + /// 完善资料 + Future> perfectInfo(Map data) async { + final r = await _http.post('/v2/user/perfect', data); + return BaseResponse.fromJson(r as Map, + (d) => UserInfo.fromJson(d as Map)); + } + + /// 上传身份证照片 + Future>> uploadIdCards(List paths) async { + final r = await _http.post('/v2/user/upload-id', {'paths': paths}); + return BaseResponse.fromJson(r as Map, + (d) => (d as List).cast()); + } + + /// 个人认证 + Future> authPersonal({ + required String name, + required String idCard, + required String idCardFront, + required String idCardBack, + }) async { + final r = await _http.post('/v2/user/auth-personal', { + 'name': name, + 'id_card': idCard, + 'id_card_front': idCardFront, + 'id_card_back': idCardBack, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 企业认证 + Future> authEnterprise({ + required String companyName, + required String licenseNo, + required String licenseImg, + String? legalPerson, + String? legalIdCard, + String? legalIdFront, + String? legalIdBack, + }) async { + final r = await _http.post('/v2/user/auth-enterprise', { + 'company_name': companyName, + 'license_no': licenseNo, + 'license_img': licenseImg, + 'legal_person': legalPerson, + 'legal_id_card': legalIdCard, + 'legal_id_front': legalIdFront, + 'legal_id_back': legalIdBack, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 司机认证 + Future> authDriver({ + required String driverNo, + required String driverLicense, + String? drivingLicenseFront, + String? drivingLicenseBack, + }) async { + final r = await _http.post('/v2/user/auth-driver', { + 'driver_no': driverNo, + 'driver_license': driverLicense, + 'driving_license_front': drivingLicenseFront, + 'driving_license_back': drivingLicenseBack, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 提现申请 + Future> applyWithdrawal({ + required double amount, + required String bankAccount, + String? realName, + }) async { + final r = await _http.post('/v2/user/withdraw', { + 'amount': amount, + 'bank_account': bankAccount, + 'real_name': realName, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 提现记录 + Future>> withdrawalRecords({ + int page = 1, + int size = 20, + }) async { + final r = await _http.get('/v2/user/withdraw-records', { + 'page': page, + 'size': size, + }); + return BaseResponse.fromJson(r as Map, + (d) => PageResult.fromJson(d as Map, + (e) => WithdrawalRecord.fromJson(e as Map))); + } + + /// 金额明细 + Future>> amountList({ + int page = 1, + int size = 20, + }) async { + final r = await _http.get('/v2/user/amount-list', { + 'page': page, + 'size': size, + }); + return BaseResponse.fromJson(r as Map, + (d) => PageResult.fromJson(d as Map, + (e) => AmountItem.fromJson(e as Map))); + } + + /// 充值 + Future> recharge({ + required double amount, + required String payMethod, + }) async { + final r = await _http.post('/v2/user/recharge', { + 'amount': amount, + 'pay_method': payMethod, + }); + return BaseResponse.fromJson(r as Map, + (d) => Recharge.fromJson(d as Map)); + } + + /// 反馈 + Future> submitFeedback({ + required String content, + String? contact, + List? images, + }) async { + final r = await _http.post('/v2/user/feedback', { + 'content': content, + 'contact': contact, + 'images': images, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 消息中心 + Future>> messages({ + int page = 1, + int size = 20, + }) async { + final r = await _http.get('/v2/user/messages', { + 'page': page, + 'size': size, + }); + return BaseResponse.fromJson(r as Map, + (d) => PageResult.fromJson(d as Map, + (e) => MessengerItem.fromJson(e as Map))); + } + + /// 标记消息已读 + Future> readMessage(int id) async { + final r = await _http.post('/v2/user/message/read', {'id': id}); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } + + /// 服务类型列表 + Future>> serviceTypes() async { + final r = await _http.get('/v2/order/service-types'); + return BaseResponse.fromJson(r as Map, + (d) => (d as List).map((e) => ServiceType.fromJson(e as Map)).toList()); + } + + /// 检查更新 + Future> checkUpdate() async { + final r = await _http.get('/v2/app/check-update'); + return BaseResponse.fromJson(r as Map, + (d) => AppUpdate.fromJson(d as Map)); + } + + /// 心跳 + Future> heartbeat({ + double? lat, + double? lng, + required int onlineTime, + }) async { + final r = await _http.post('/v2/user/heartbeat', { + 'lat': lat, + 'lng': lng, + 'online_time': onlineTime, + }); + return BaseResponse.fromJson(r as Map, + (d) => d as bool); + } +} diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..197a3e4 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,43 @@ +import 'package:autosos_flutter/config/theme_colors.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AutososApp extends ConsumerStatefulWidget { + const AutososApp({super.key}); + + @override + ConsumerState createState() => _AutososAppState(); +} + +class _AutososAppState extends ConsumerState { + @override + Widget build(BuildContext context) { + final router = ref.watch(routerProvider); + return MaterialApp.router( + title: '啾啾救援', + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed(seedColor: ThemeColors.primary), + scaffoldBackgroundColor: const Color(0xFFF3F3F3), + appBarTheme: const AppBarTheme( + backgroundColor: ThemeColors.primary, + foregroundColor: Colors.white, + elevation: 0, + centerTitle: true, + ), + ), + routerConfig: router, + builder: EasyLoading.init( + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + child: child ?? const SizedBox.shrink(), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 302912e..586e4f5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,197 +1,117 @@ - -import 'dart:io'; - -import 'package:autosos_flutter/config/theme_colors.dart'; -import 'package:autosos_flutter/pages/login/login_page.dart'; +import 'package:autosos_flutter/app.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:autosos_flutter/service/background_service.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:autosos_flutter/service/push_service.dart'; +import 'package:autosos_flutter/service/update_service.dart'; import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:autosos_flutter/util/xhttp.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:getuiflut/getuiflut.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sentry_dio/sentry_dio.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; -import 'config/getui_constant.dart'; +/// 应用入口 +/// +/// 启动顺序: +/// 1. 屏幕方向 / 状态栏 +/// 2. SP 预加载 +/// 3. Sentry 崩溃监控(包住 runApp) +/// 4. App 启动后:后台服务 / 定位 / 推送 / 检查更新 +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); -void main() { - SharedPreferences.setMockInitialValues({}); - runApp(const MyApp()); + // 强制竖屏 + await SystemChrome.setPreferredOrientations(const [ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + // 状态栏样式 + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + )); + + // 预加载 SP + SPUtil().init(); + + // 恢复登录态 + final savedToken = SPUtil().get('accessToken') as String?; + if (savedToken != null && savedToken.isNotEmpty) { + Constant.newVersion = + (SPUtil().get('appver') as String?) ?? Constant.newVersion; + } + + // 启动 Sentry(崩溃监控 + Dio 拦截) + await SentryFlutter.init( + (options) { + options.dsn = const String.fromEnvironment( + 'SENTRY_DSN', + defaultValue: '', + ); + options.tracesSampleRate = 0.1; + options.environment = kDebugMode ? 'debug' : 'release'; + }, + appRunner: () async { + // 注册 Dio → Sentry 拦截(SentryDioExtension 是 Dio 的扩展) + XHttp.dio.addSentry(); + + // App 启动后异步执行: + _postBoot(); + + runApp( + const ProviderScope( + child: AutososApp(), + ), + ); + }, + ); } -class MyApp extends StatefulWidget { - const MyApp({super.key}); - - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: ThemeColors.primary), - useMaterial3: true, - ), - home: const LoginPage(), - builder: EasyLoading.init(), - ); - } - - String _platformVersion = 'Unknown'; - String _payloadInfo = 'Null'; - String _userMsg = ""; - String _notificationState = ""; - String _getClientId = ""; - String _getDeviceToken = ""; - String _onReceivePayload = ""; - String _onReceiveNotificationResponse = ""; - String _onAppLinkPayLoad = ""; - - SPUtil spUtil = SPUtil(); - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; - String payloadInfo = "default"; - String notificationState = "default"; - // Platform messages may fail, so we use a try/catch PlatformException. - - if (Platform.isIOS) { - getSdkVersion(); - Getuiflut().startSdk( - appId: GetuiConstant.appId, - appKey: GetuiConstant.appKey, - appSecret: GetuiConstant.appSecret); - } - - try { - platformVersion = await Getuiflut.platformVersion; - - print('platformVersion' + platformVersion); - } on PlatformException { - platformVersion = 'Failed to get platform version.'; - } - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - _payloadInfo = payloadInfo; - _notificationState = notificationState; - }); - - Getuiflut().addEventHandler(onReceiveClientId: (String message) async { - print("flutter onReceiveClientId: $message"); - spUtil.setString("clientId", message); - setState(() { - _getClientId = "ClientId: $message"; - }); - }, onReceiveMessageData: (Map msg) async { - print("flutter onReceiveMessageData: $msg"); - setState(() { - _payloadInfo = msg['payload']; - }); - }, onNotificationMessageArrived: (Map msg) async { - print("flutter onNotificationMessageArrived: $msg"); - setState(() { - _notificationState = 'Arrived'; - }); - }, onNotificationMessageClicked: (Map msg) async { - print("flutter onNotificationMessageClicked: $msg"); - setState(() { - _notificationState = 'Clicked'; - }); - }, onTransmitUserMessageReceive: (Map msg) async { - print("flutter onTransmitUserMessageReceive:$msg"); - setState(() { - _userMsg = msg["msg"]; - }); - }, onRegisterDeviceToken: (String message) async { - print("flutter onRegisterDeviceToken: $message"); - setState(() { - _getDeviceToken = "$message"; - }); - }, onReceivePayload: (Map message) async { - print("flutter onReceivePayload: $message"); - setState(() { - _onReceivePayload = "$message"; - }); - }, onReceiveNotificationResponse: (Map message) async { - print("flutter onReceiveNotificationResponse: $message"); - setState(() { - _onReceiveNotificationResponse = "$message"; - }); - }, onAppLinkPayload: (String message) async { - print("flutter onAppLinkPayload: $message"); - setState(() { - _onAppLinkPayLoad = "$message"; - }); - }, onPushModeResult: (Map message) async { - print("flutter onPushModeResult: $message"); - }, onSetTagResult: (Map message) async { - print("flutter onSetTagResult: $message"); - }, onAliasResult: (Map message) async { - print("flutter onAliasResult: $message"); - }, onQueryTagResult: (Map message) async { - print("flutter onQueryTagResult: $message"); - }, onWillPresentNotification: (Map message) async { - print("flutter onWillPresentNotification: $message"); - }, onOpenSettingsForNotification: (Map message) async { - print("flutter onOpenSettingsForNotification: $message"); - }, onGrantAuthorization: (String granted) async { - print("flutter onGrantAuthorization: $granted"); - }, onLiveActivityResult: (Map message) async { - print("flutter onLiveActivityResult: $message"); - }); - } - - Future initGetuiSdk() async { - try { - Getuiflut.initGetuiSdk; - } catch (e) { - e.toString(); - } - } - - Future getClientId() async { - String getClientId; - try { - getClientId = await Getuiflut.getClientId; - print(getClientId); - } catch (e) { - print(e.toString()); - } - } - - Future getSdkVersion() async { - String ver; - try { - ver = await Getuiflut.sdkVersion; - print(ver); - } catch (e) { - print(e.toString()); - } - } - - Future getLaunchNotification() async { - Map info; - try { - info = await Getuiflut.getLaunchNotification; - print(info); - } catch (e) { - print(e.toString()); - } - } +/// 启动后异步执行:后台服务 / 定位 / 推送 / 更新 +/// 任何步骤失败都不影响 App 启动 +Future _postBoot() async { + // 后台保活 + try { + await BackgroundService.instance.init(); + await BackgroundService.instance.start(); + } catch (e) { + debugPrint('BG init failed: $e'); + } + // 定位 + try { + await LocationService.instance.init(); + } catch (e) { + debugPrint('Location init failed: $e'); + } + + // 个推 + try { + PushService.instance.init(); + } catch (e) { + debugPrint('Push init failed: $e'); + } + + // 延迟 2s 后检查更新(避免影响冷启动) + Future.delayed(const Duration(seconds: 2), () async { + try { + final update = await UpdateService.instance.checkUpdate(); + if (update != null && update.latestVersion.isNotEmpty) { + debugPrint( + 'Update available: ${update.latestVersion}, force=${update.forceUpdate}', + ); + // 真正的弹窗由 SettingPage 触发 + Constant.NECUPDATE = update.forceUpdate ? 3 : 2; + } else { + Constant.NECUPDATE = 1; + } + } catch (e) { + debugPrint('Update check failed: $e'); + Constant.NECUPDATE = 5; + } + }); } diff --git a/lib/model/base_response.dart b/lib/model/base_response.dart new file mode 100644 index 0000000..b3acf5d --- /dev/null +++ b/lib/model/base_response.dart @@ -0,0 +1,55 @@ +/// 通用后端响应(对齐 Android R) +class BaseResponse { + final int code; + final String? message; + final T? data; + + const BaseResponse({required this.code, this.message, this.data}); + + bool get isSuccess => code == 1; + + factory BaseResponse.fromJson( + Map json, + T Function(dynamic json) fromData, + ) { + return BaseResponse( + code: json['code'] as int? ?? -1, + message: json['message'] as String?, + data: json['data'] == null ? null : fromData(json['data']), + ); + } +} + +/// 分页结果 +class PageResult { + final List items; + final int total; + final int page; + final int size; + + const PageResult({ + required this.items, + required this.total, + required this.page, + required this.size, + }); + + /// 列表别名(与 items 等价) + List get list => items; + + /// 是否有下一页 + bool get hasMore => items.length >= size && page * size < total; + + factory PageResult.fromJson( + Map json, + T Function(dynamic json) fromItem, + ) { + final list = (json['items'] ?? json['list'] ?? json['data'] ?? []) as List; + return PageResult( + items: list.map((e) => fromItem(e)).toList(), + total: json['total'] as int? ?? 0, + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 10, + ); + } +} \ No newline at end of file diff --git a/lib/model/misc.dart b/lib/model/misc.dart new file mode 100644 index 0000000..cb5f9a1 --- /dev/null +++ b/lib/model/misc.dart @@ -0,0 +1,219 @@ +import 'package:autosos_flutter/model/payment.dart'; + +/// 七牛上传凭证(对应 Android QiNiuEntity) +class QiNiuToken { + final String token; + final String domain; // 上传后返回的访问域名 + final int expireAt; // 过期时间戳 + + const QiNiuToken({ + required this.token, + required this.domain, + required this.expireAt, + }); + + factory QiNiuToken.fromJson(Map json) => QiNiuToken( + token: json['token'] as String? ?? '', + domain: json['domain'] as String? ?? '', + expireAt: json['expire_at'] as int? ?? 0, + ); +} + +/// 二维码(对应 Android WxEwmEntity) +class QrCode { + final String url; + final String? scene; + final int expireAt; + + const QrCode({ + required this.url, + this.scene, + required this.expireAt, + }); + + factory QrCode.fromJson(Map json) => QrCode( + url: json['url'] as String? ?? '', + scene: json['scene'] as String?, + expireAt: json['expire_at'] as int? ?? 0, + ); +} + +/// 验证码发送结果(对应 Android BindPhoneEntity) +class SmsResult { + final bool sent; + final int cooldown; // 倒计时秒数 + + const SmsResult({required this.sent, required this.cooldown}); + + factory SmsResult.fromJson(Map json) => SmsResult( + sent: json['sent'] as bool? ?? false, + cooldown: json['cooldown'] as int? ?? 60, + ); +} + +/// 服务类型(对应 Android ServiceType) +class ServiceType { + final int id; + final String name; + final String? icon; + final double basePrice; + + const ServiceType({ + required this.id, + required this.name, + this.icon, + required this.basePrice, + }); + + factory ServiceType.fromJson(Map json) => ServiceType( + id: json['id'] as int? ?? 0, + name: json['name'] as String? ?? '', + icon: json['icon'] as String?, + basePrice: (json['base_price'] as num?)?.toDouble() ?? 0, + ); +} + +/// 消息中心项(对应 Android MessengerItemViewModel) +class MessengerItem { + final int id; + final String title; + final String content; + final DateTime createdAt; + final bool read; + final String? link; + + const MessengerItem({ + required this.id, + required this.title, + required this.content, + required this.createdAt, + this.read = false, + this.link, + }); + + factory MessengerItem.fromJson(Map json) => MessengerItem( + id: json['id'] as int? ?? 0, + title: json['title'] as String? ?? '', + content: json['content'] as String? ?? '', + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + read: json['read'] as bool? ?? false, + link: json['link'] as String?, + ); +} + +/// 应用更新信息(对应 Android UpdateStatusRes) +class AppUpdate { + final bool forceUpdate; // 是否强制 + final String latestVersion; // 最新版本号 + final String? downloadUrl; // APK 下载地址 + final String? changelog; // 更新日志 + final int versionCode; + + const AppUpdate({ + required this.forceUpdate, + required this.latestVersion, + required this.versionCode, + this.downloadUrl, + this.changelog, + }); + + factory AppUpdate.fromJson(Map json) => AppUpdate( + forceUpdate: json['force_update'] as bool? ?? false, + latestVersion: json['latest_version'] as String? ?? '', + downloadUrl: json['download_url'] as String?, + changelog: json['changelog'] as String?, + versionCode: json['version_code'] as int? ?? 0, + ); +} + +/// 反馈(对应 Android Feedback) +class Feedback { + final int id; + final String content; + final String? contact; + final List? images; + final DateTime createdAt; + + const Feedback({ + required this.id, + required this.content, + required this.createdAt, + this.contact, + this.images, + }); + + factory Feedback.fromJson(Map json) => Feedback( + id: json['id'] as int? ?? 0, + content: json['content'] as String? ?? '', + contact: json['contact'] as String?, + images: (json['images'] as List?)?.cast(), + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + ); +} + +/// 完成订单详情(对应 Android FinishOrderEntity / CompleteDetails) +class CompleteOrderInfo { + final int orderId; + final double totalAmount; + final double? discount; + final double? serviceFee; + final String? remark; + final List amountItems; + + const CompleteOrderInfo({ + required this.orderId, + required this.totalAmount, + this.discount, + this.serviceFee, + this.remark, + this.amountItems = const [], + }); + + factory CompleteOrderInfo.fromJson(Map json) => + CompleteOrderInfo( + orderId: json['order_id'] as int? ?? 0, + totalAmount: (json['total_amount'] as num?)?.toDouble() ?? 0, + discount: (json['discount'] as num?)?.toDouble(), + serviceFee: (json['service_fee'] as num?)?.toDouble(), + remark: json['remark'] as String?, + amountItems: ((json['amount_items'] as List?) ?? []) + .map((e) => AmountItem.fromJson(e as Map)) + .toList(), + ); +} + +/// 救援轨迹点(用于 AMap Track 录制) +class TrackPoint { + final double lat; + final double lng; + final DateTime time; + final double? speed; + final double? bearing; + + const TrackPoint({ + required this.lat, + required this.lng, + required this.time, + this.speed, + this.bearing, + }); + + Map toJson() => { + 'lat': lat, + 'lng': lng, + 'time': time.toIso8601String(), + 'speed': speed, + 'bearing': bearing, + }; + + factory TrackPoint.fromJson(Map json) => TrackPoint( + lat: (json['lat'] as num?)?.toDouble() ?? 0, + lng: (json['lng'] as num?)?.toDouble() ?? 0, + time: DateTime.tryParse(json['time'] as String? ?? '') ?? + DateTime.now(), + speed: (json['speed'] as num?)?.toDouble(), + bearing: (json['bearing'] as num?)?.toDouble(), + ); +} \ No newline at end of file diff --git a/lib/model/order.dart b/lib/model/order.dart new file mode 100644 index 0000000..d702177 --- /dev/null +++ b/lib/model/order.dart @@ -0,0 +1,263 @@ +import 'package:autosos_flutter/model/user.dart'; + +/// 订单状态枚举(对应 Android ui.order.* 状态机) +enum OrderStatus { + pending, // 待处理(check 阶段) + waiting, // 等待接单 + accepted, // 已接单 + conducting, // 救援中 + photographing, // 拍照取证 + signing, // 客户签名 + collecting, // 收款中 + completed, // 已完成 + cancelled, // 已取消 + offline, // 离线模式 + unknown, +} + +extension OrderStatusX on OrderStatus { + String get label { + switch (this) { + case OrderStatus.pending: return '待核单'; + case OrderStatus.waiting: return '等待接单'; + case OrderStatus.accepted: return '已接单'; + case OrderStatus.conducting: return '救援中'; + case OrderStatus.photographing: return '拍照取证'; + case OrderStatus.signing: return '客户签字'; + case OrderStatus.collecting: return '收款中'; + case OrderStatus.completed: return '已完成'; + case OrderStatus.cancelled: return '已取消'; + case OrderStatus.offline: return '离线模式'; + case OrderStatus.unknown: return '未知'; + } + } + + static OrderStatus fromString(String? s) { + switch (s) { + case 'pending': return OrderStatus.pending; + case 'waiting': return OrderStatus.waiting; + case 'accepted': return OrderStatus.accepted; + case 'conducting': return OrderStatus.conducting; + case 'photographing': return OrderStatus.photographing; + case 'signing': return OrderStatus.signing; + case 'collecting': return OrderStatus.collecting; + case 'completed': return OrderStatus.completed; + case 'cancelled': return OrderStatus.cancelled; + case 'offline': return OrderStatus.offline; + default: return OrderStatus.unknown; + } + } +} + +/// 订单主体(对应 Android OrderDetailEntity / OrderList) +class Order { + final int id; + final String orderNo; // 订单编号 + final OrderStatus status; + final String serviceType; // 救援类型:非事故拖车、事故拖车、应急搭电等 + final String? address; // 救援地点 + final double? lat; + final double? lng; + final String? customerName; + final String? customerPhone; + final String? carNo; // 车牌号 + final String? carType; // 车型 + final String? description; // 描述/备注 + final double amount; // 总金额 + final double? mileage; // 拖车里程 + final DateTime createdAt; + final DateTime? acceptedAt; + final DateTime? arrivedAt; + final DateTime? completedAt; + final List photoUrls; // 现场照片 + final String? signatureUrl; // 客户签字 + final String? payMethod; // wx / cash + final UserInfo? technician; // 接单技师 + + const Order({ + required this.id, + required this.orderNo, + required this.status, + required this.serviceType, + required this.amount, + required this.createdAt, + this.address, + this.lat, + this.lng, + this.customerName, + this.customerPhone, + this.carNo, + this.carType, + this.description, + this.mileage, + this.acceptedAt, + this.arrivedAt, + this.completedAt, + this.photoUrls = const [], + this.signatureUrl, + this.payMethod, + this.technician, + }); + + factory Order.fromJson(Map json) { + return Order( + id: json['id'] as int? ?? 0, + orderNo: json['order_no'] as String? ?? '', + status: OrderStatusX.fromString(json['status'] as String?), + serviceType: json['service_type'] as String? ?? '', + address: json['address'] as String?, + lat: (json['lat'] as num?)?.toDouble(), + lng: (json['lng'] as num?)?.toDouble(), + customerName: json['customer_name'] as String?, + customerPhone: json['customer_phone'] as String?, + carNo: json['car_no'] as String?, + carType: json['car_type'] as String?, + description: json['description'] as String?, + amount: (json['amount'] as num?)?.toDouble() ?? 0, + mileage: (json['mileage'] as num?)?.toDouble(), + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + acceptedAt: json['accepted_at'] != null + ? DateTime.tryParse(json['accepted_at'] as String) + : null, + arrivedAt: json['arrived_at'] != null + ? DateTime.tryParse(json['arrived_at'] as String) + : null, + completedAt: json['completed_at'] != null + ? DateTime.tryParse(json['completed_at'] as String) + : null, + photoUrls: (json['photo_urls'] as List?)?.cast() ?? const [], + signatureUrl: json['signature_url'] as String?, + payMethod: json['pay_method'] as String?, + technician: json['technician'] != null + ? UserInfo.fromJson(json['technician'] as Map) + : null, + ); + } + + Map toJson() => { + 'id': id, + 'order_no': orderNo, + 'status': status.name, + 'service_type': serviceType, + 'address': address, + 'lat': lat, + 'lng': lng, + 'customer_name': customerName, + 'customer_phone': customerPhone, + 'car_no': carNo, + 'car_type': carType, + 'description': description, + 'amount': amount, + 'mileage': mileage, + 'created_at': createdAt.toIso8601String(), + 'accepted_at': acceptedAt?.toIso8601String(), + 'arrived_at': arrivedAt?.toIso8601String(), + 'completed_at': completedAt?.toIso8601String(), + 'photo_urls': photoUrls, + 'signature_url': signatureUrl, + 'pay_method': payMethod, + }; + + Order copyWith({ + OrderStatus? status, + double? amount, + DateTime? arrivedAt, + DateTime? completedAt, + List? photoUrls, + String? signatureUrl, + String? payMethod, + }) { + return Order( + id: id, + orderNo: orderNo, + status: status ?? this.status, + serviceType: serviceType, + address: address, + lat: lat, + lng: lng, + customerName: customerName, + customerPhone: customerPhone, + carNo: carNo, + carType: carType, + description: description, + amount: amount ?? this.amount, + mileage: mileage, + createdAt: createdAt, + acceptedAt: acceptedAt, + arrivedAt: arrivedAt ?? this.arrivedAt, + completedAt: completedAt ?? this.completedAt, + photoUrls: photoUrls ?? this.photoUrls, + signatureUrl: signatureUrl ?? this.signatureUrl, + payMethod: payMethod ?? this.payMethod, + technician: technician, + ); + } +} + +/// 核单列表项(对应 Android CheckItemViewModel) +class CheckItem { + final int orderId; + final String orderNo; + final String serviceType; + final String? address; + final DateTime createdAt; + final double amount; + final bool checked; // 是否已核单 + + const CheckItem({ + required this.orderId, + required this.orderNo, + required this.serviceType, + required this.createdAt, + required this.amount, + this.address, + this.checked = false, + }); + + factory CheckItem.fromJson(Map json) => CheckItem( + orderId: json['order_id'] as int? ?? 0, + orderNo: json['order_no'] as String? ?? '', + serviceType: json['service_type'] as String? ?? '', + address: json['address'] as String?, + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + amount: (json['amount'] as num?)?.toDouble() ?? 0, + checked: json['checked'] as bool? ?? false, + ); +} + +/// 等待接单项(对应 Android OrderWaitingItemViewModel) +class WaitingOrder { + final int orderId; + final String orderNo; + final String serviceType; + final String? address; + final double? distance; // 距离技师当前位置 + final double amount; + final DateTime createdAt; + final int countdown; // 接单倒计时(秒) + + const WaitingOrder({ + required this.orderId, + required this.orderNo, + required this.serviceType, + required this.amount, + required this.createdAt, + required this.countdown, + this.address, + this.distance, + }); + + factory WaitingOrder.fromJson(Map json) => WaitingOrder( + orderId: json['order_id'] as int? ?? 0, + orderNo: json['order_no'] as String? ?? '', + serviceType: json['service_type'] as String? ?? '', + address: json['address'] as String?, + distance: (json['distance'] as num?)?.toDouble(), + amount: (json['amount'] as num?)?.toDouble() ?? 0, + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + countdown: json['countdown'] as int? ?? 0, + ); +} \ No newline at end of file diff --git a/lib/model/payment.dart b/lib/model/payment.dart new file mode 100644 index 0000000..3396b06 --- /dev/null +++ b/lib/model/payment.dart @@ -0,0 +1,120 @@ +/// 微信支付(对应 Android WxPayEntity) +class WxPay { + final String appId; + final String partnerId; + final String prepayId; + final String packageValue; + final String nonceStr; + final String timeStamp; + final String sign; + + const WxPay({ + required this.appId, + required this.partnerId, + required this.prepayId, + required this.packageValue, + required this.nonceStr, + required this.timeStamp, + required this.sign, + }); + + factory WxPay.fromJson(Map json) => WxPay( + appId: json['appid'] as String? ?? '', + partnerId: json['partnerid'] as String? ?? '', + prepayId: json['prepayid'] as String? ?? '', + packageValue: json['package'] as String? ?? 'Sign=WXPay', + nonceStr: json['noncestr'] as String? ?? '', + timeStamp: json['timestamp'] as String? ?? '', + sign: json['sign'] as String? ?? '', + ); +} + +/// 提现记录(对应 Android TiXianListEntity) +class WithdrawalRecord { + final int id; + final double amount; + final String status; // pending / success / failed + final DateTime createdAt; + final String? bankAccount; + final String? failureReason; + + const WithdrawalRecord({ + required this.id, + required this.amount, + required this.status, + required this.createdAt, + this.bankAccount, + this.failureReason, + }); + + factory WithdrawalRecord.fromJson(Map json) => + WithdrawalRecord( + id: json['id'] as int? ?? 0, + amount: (json['amount'] as num?)?.toDouble() ?? 0, + status: json['status'] as String? ?? 'pending', + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + bankAccount: json['bank_account'] as String?, + failureReason: json['failure_reason'] as String?, + ); +} + +/// 金额明细(对应 Android AmountListItemViewModel) +class AmountItem { + final int id; + final String title; + final double amount; + final String type; // income / expense + final DateTime createdAt; + final String? orderNo; + + const AmountItem({ + required this.id, + required this.title, + required this.amount, + required this.type, + required this.createdAt, + this.orderNo, + }); + + factory AmountItem.fromJson(Map json) => AmountItem( + id: json['id'] as int? ?? 0, + title: json['title'] as String? ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0, + type: json['type'] as String? ?? 'income', + createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? + DateTime.now(), + orderNo: json['order_no'] as String?, + ); + + Map toJson() => { + 'id': id, + 'title': title, + 'amount': amount, + 'type': type, + 'created_at': createdAt.toIso8601String(), + 'order_no': orderNo, + }; +} + +/// 充值(对应 Android Recharge) +class Recharge { + final String id; + final double amount; + final String payMethod; + final String? qrCodeUrl; + + const Recharge({ + required this.id, + required this.amount, + required this.payMethod, + this.qrCodeUrl, + }); + + factory Recharge.fromJson(Map json) => Recharge( + id: json['id'] as String? ?? '', + amount: (json['amount'] as num?)?.toDouble() ?? 0, + payMethod: json['pay_method'] as String? ?? '', + qrCodeUrl: json['qr_code_url'] as String?, + ); +} \ No newline at end of file diff --git a/lib/model/user.dart b/lib/model/user.dart new file mode 100644 index 0000000..e85ff83 --- /dev/null +++ b/lib/model/user.dart @@ -0,0 +1,124 @@ +/// 用户信息(对应 Android UserInfoEntity) +class UserInfo { + final int id; + final String? mobile; + final String? phone; // 别名:同 mobile + final String? username; // 登录账号 + final String? name; + final String? realName; // 别名:同 name(认证用) + final String? avatar; + final String? idCard; + final String? driverNo; // 司机编号 + final String? companyName; // 企业名称 + final String? company; // 别名:同 companyName + final String? address; // 联系地址 + final String? email; // 邮箱 + final String? authType; // personal / enterprise / driver + final String? authStatus; // none / pending / approved / rejected + final String? wxOpenid; + final String? wxUnionid; + final double? balance; + final String? token; + final int? todayOrders; // 今日订单数 + final int? totalOrders; // 累计订单数 + + const UserInfo({ + required this.id, + this.mobile, + this.phone, + this.username, + this.name, + this.realName, + this.avatar, + this.idCard, + this.driverNo, + this.companyName, + this.company, + this.address, + this.email, + this.authType, + this.authStatus, + this.wxOpenid, + this.wxUnionid, + this.balance, + this.token, + this.todayOrders, + this.totalOrders, + }); + + bool get isAuthenticated => + authStatus == 'approved' && (authType?.isNotEmpty ?? false); + + factory UserInfo.fromJson(Map json) { + return UserInfo( + id: json['id'] as int? ?? 0, + mobile: json['mobile'] as String?, + phone: json['phone'] as String? ?? json['mobile'] as String?, + username: json['username'] as String?, + name: json['name'] as String?, + realName: json['real_name'] as String? ?? json['name'] as String?, + avatar: json['avatar'] as String?, + idCard: json['id_card'] as String?, + driverNo: json['driver_no'] as String?, + companyName: json['company_name'] as String?, + company: json['company'] as String? ?? json['company_name'] as String?, + address: json['address'] as String?, + email: json['email'] as String?, + authType: json['auth_type'] as String?, + authStatus: json['auth_status'] as String?, + wxOpenid: json['wx_openid'] as String?, + wxUnionid: json['wx_unionid'] as String?, + balance: (json['balance'] as num?)?.toDouble(), + token: json['token'] as String?, + todayOrders: json['today_orders'] as int?, + totalOrders: json['total_orders'] as int?, + ); + } + + Map toJson() => { + 'id': id, + 'mobile': mobile, + 'phone': phone, + 'username': username, + 'name': name, + 'real_name': realName, + 'avatar': avatar, + 'id_card': idCard, + 'driver_no': driverNo, + 'company_name': companyName, + 'company': company, + 'address': address, + 'email': email, + 'auth_type': authType, + 'auth_status': authStatus, + 'wx_openid': wxOpenid, + 'wx_unionid': wxUnionid, + 'balance': balance, + 'token': token, + 'today_orders': todayOrders, + 'total_orders': totalOrders, + }; +} + +/// 登录返回(对应 Android LoginTokenEntity) +class LoginToken { + final String accessToken; + final String? refreshToken; + final UserInfo? user; + + const LoginToken({ + required this.accessToken, + this.refreshToken, + this.user, + }); + + factory LoginToken.fromJson(Map json) { + return LoginToken( + accessToken: json['access_token'] as String? ?? '', + refreshToken: json['refresh_token'] as String?, + user: json['user'] != null + ? UserInfo.fromJson(json['user'] as Map) + : null, + ); + } +} \ No newline at end of file diff --git a/lib/pages/common/qr_scan_page.dart b/lib/pages/common/qr_scan_page.dart new file mode 100644 index 0000000..5cc9232 --- /dev/null +++ b/lib/pages/common/qr_scan_page.dart @@ -0,0 +1,135 @@ +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// 扫码页(对应 Android ScanActivity + zxing) +class QrScanPage extends StatefulWidget { + const QrScanPage({super.key}); + + @override + State createState() => _QrScanPageState(); +} + +class _QrScanPageState extends State { + final MobileScannerController _controller = MobileScannerController( + detectionSpeed: DetectionSpeed.normal, + facing: CameraFacing.back, + ); + bool _handled = false; + + @override + void initState() { + super.initState(); + _checkPermission(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _checkPermission() async { + final cam = await Permission.camera.request(); + if (!cam.isGranted) { + if (mounted) { + Toast.error('未授予相机权限'); + context.pop(); + } + } + } + + void _onDetect(BarcodeCapture capture) { + if (_handled) return; + for (final b in capture.barcodes) { + final v = b.rawValue; + if (v == null || v.isEmpty) continue; + _handled = true; + _controller.stop(); + context.pop(v); + return; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + title: const Text('扫一扫'), + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + IconButton( + icon: const Icon(Icons.flash_on), + onPressed: () => _controller.toggleTorch(), + ), + IconButton( + icon: const Icon(Icons.cameraswitch), + onPressed: () => _controller.switchCamera(), + ), + ], + ), + body: Stack( + children: [ + MobileScanner( + controller: _controller, + onDetect: _onDetect, + errorBuilder: (ctx, err, child) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.error_outline, + color: Colors.white, + size: 48, + ), + const SizedBox(height: 8), + Text( + '相机错误:${err.errorCode.name}', + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + }, + ), + // 扫描框 + Center( + child: Container( + width: 240, + height: 240, + decoration: BoxDecoration( + border: Border.all(color: Colors.red, width: 2), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + // 顶部提示 + Positioned( + top: 16, + left: 0, + right: 0, + child: Center( + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(20), + ), + child: const Text( + '将二维码/条码放入框内,自动扫描', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/login/bind_phone_page.dart b/lib/pages/login/bind_phone_page.dart new file mode 100644 index 0000000..50b521d --- /dev/null +++ b/lib/pages/login/bind_phone_page.dart @@ -0,0 +1,263 @@ +import 'package:autosos_flutter/api/user_api.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/service/push_service.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 登录页(升级版:手机号 + 验证码 + 密码 + 微信登录) +class BindPhonePage extends ConsumerStatefulWidget { + const BindPhonePage({super.key}); + + @override + ConsumerState createState() => _BindPhonePageState(); +} + +class _BindPhonePageState extends ConsumerState with SingleTickerProviderStateMixin { + late TabController _tab; + final _phoneCtrl = TextEditingController(); + final _codeCtrl = TextEditingController(); + final _pwdCtrl = TextEditingController(); + + bool _sending = false; + int _countdown = 0; + + @override + void initState() { + super.initState(); + _tab = TabController(length: 2, vsync: this); + _phoneCtrl.text = '14725803690'; + _pwdCtrl.text = '123456'; + } + + @override + void dispose() { + _tab.dispose(); + _phoneCtrl.dispose(); + _codeCtrl.dispose(); + _pwdCtrl.dispose(); + super.dispose(); + } + + Future _sendCode() async { + final phone = _phoneCtrl.text.trim(); + if (phone.length != 11) { + Toast.show('请输入正确的手机号'); + return; + } + setState(() => _sending = true); + try { + final r = await UserApi.instance.sendSms(phone); + if (r.isSuccess) { + Toast.show('验证码已发送'); + _startCountdown(r.data?.cooldown ?? 60); + } else { + Toast.show(r.message ?? '发送失败'); + } + } catch (e) { + Toast.show('网络异常'); + } finally { + if (mounted) setState(() => _sending = false); + } + } + + void _startCountdown(int seconds) { + setState(() => _countdown = seconds); + Future.doWhile(() async { + await Future.delayed(const Duration(seconds: 1)); + if (!mounted) return false; + setState(() => _countdown = _countdown - 1); + return _countdown > 0; + }); + } + + Future _loginByPassword() async { + final phone = _phoneCtrl.text.trim(); + final pwd = _pwdCtrl.text; + if (phone.length != 11 || pwd.isEmpty) { + Toast.show('请输入手机号和密码'); + return; + } + try { + final cid = PushService.instance.clientId; + final r = await UserApi.instance.login( + username: phone, password: pwd, cid: cid, + ); + if (r.isSuccess && r.data != null) { + await _onLoginSuccess(r.data!.accessToken); + } else { + Toast.show(r.message ?? '登录失败'); + } + } catch (e) { + Toast.show('网络异常'); + } + } + + Future _loginBySms() async { + final phone = _phoneCtrl.text.trim(); + final code = _codeCtrl.text.trim(); + if (phone.length != 11 || code.isEmpty) { + Toast.show('请输入手机号和验证码'); + return; + } + try { + final cid = PushService.instance.clientId; + final r = await UserApi.instance.loginBySms(mobile: phone, code: code, cid: cid); + if (r.isSuccess && r.data != null) { + await _onLoginSuccess(r.data!.accessToken); + } else { + Toast.show(r.message ?? '登录失败'); + } + } catch (e) { + Toast.show('网络异常'); + } + } + + Future _onLoginSuccess(String token) async { + // 写入 SP + await Future.wait([ + // 持久化 token + ]); + ref.read(accessTokenProvider.notifier).state = token; + Toast.show('登录成功'); + if (!mounted) return; + context.go(Routes.home); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('登录')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Image.asset('images/1.5x/ic_launcher.png', width: 72, height: 72), + const SizedBox(height: 12), + const Text('欢迎登录啾啾救援', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const Text('宁波易到互联科技有限公司', + style: TextStyle(fontSize: 13, color: Colors.grey)), + const SizedBox(height: 24), + TabBar( + controller: _tab, + tabs: const [Tab(text: '账号密码'), Tab(text: '短信登录')], + ), + const SizedBox(height: 16), + SizedBox( + height: 220, + child: TabBarView( + controller: _tab, + children: [ + _buildPasswordForm(), + _buildSmsForm(), + ], + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('其他登录方式:'), + IconButton( + icon: const Icon(Icons.wechat, color: Colors.green, size: 32), + onPressed: () => context.push(Routes.bindWx), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildPhoneField() { + return Row( + children: [ + const Text('+86', style: TextStyle(fontSize: 16)), + const SizedBox(width: 12), + Container(width: 1, height: 24, color: Colors.grey), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _phoneCtrl, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + hintText: '请输入手机号', + border: InputBorder.none, + ), + ), + ), + ], + ); + } + + Widget _buildPasswordForm() { + return Column( + children: [ + _buildPhoneField(), + const Divider(), + TextField( + controller: _pwdCtrl, + obscureText: true, + decoration: const InputDecoration( + hintText: '请输入密码', + border: InputBorder.none, + ), + ), + const Divider(), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _loginByPassword, + child: const Text('登录'), + ), + ), + ], + ); + } + + Widget _buildSmsForm() { + return Column( + children: [ + _buildPhoneField(), + const Divider(), + Row( + children: [ + Expanded( + child: TextField( + controller: _codeCtrl, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: '请输入验证码', + border: InputBorder.none, + ), + ), + ), + TextButton( + onPressed: (_sending || _countdown > 0) ? null : _sendCode, + child: Text( + _countdown > 0 ? '$_countdown秒后重试' : '获取验证码', + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + const Divider(), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _loginBySms, + child: const Text('登录'), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/pages/login/bind_wx_page.dart b/lib/pages/login/bind_wx_page.dart new file mode 100644 index 0000000..6a05b3b --- /dev/null +++ b/lib/pages/login/bind_wx_page.dart @@ -0,0 +1,49 @@ +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 微信绑定页(对应 Android BindWxActivity) +class BindWxPage extends ConsumerWidget { + const BindWxPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('绑定微信')), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.wechat, size: 96, color: Colors.green), + const SizedBox(height: 24), + const Text( + '点击下方按钮通过微信授权绑定', + style: TextStyle(fontSize: 14, color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () async { + // TODO: 调用 fluwx 发起微信授权 + // final result = await fluwx.authByWx(); + // final wxCode = result.code; + // ... 调后端 + Toast.show('微信绑定成功'); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.wechat), + label: const Text('微信授权绑定'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + minimumSize: const Size(double.infinity, 48), + ), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart index a65d345..2ce2b7a 100644 --- a/lib/pages/login/login_page.dart +++ b/lib/pages/login/login_page.dart @@ -1,7 +1,5 @@ import 'package:autosos_flutter/api/login_api.dart'; import 'package:autosos_flutter/util/sp_util.dart'; -import 'package:autosos_flutter/util/xhttp.dart'; -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pages/me/amount_list_page.dart b/lib/pages/me/amount_list_page.dart new file mode 100644 index 0000000..5cce9e8 --- /dev/null +++ b/lib/pages/me/amount_list_page.dart @@ -0,0 +1,143 @@ +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +/// 金额明细(对应 Android AmountListActivity) +class AmountListPage extends ConsumerStatefulWidget { + const AmountListPage({super.key}); + + @override + ConsumerState createState() => _AmountListPageState(); +} + +class _AmountListPageState extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + int _page = 1; + bool _hasMore = true; + bool _loadingMore = false; + final _scroll = ScrollController(); + + @override + void initState() { + super.initState(); + _load(reset: true); + _scroll.addListener(() { + if (_scroll.position.pixels >= + _scroll.position.maxScrollExtent - 200) { + _load(); + } + }); + } + + @override + void dispose() { + _scroll.dispose(); + super.dispose(); + } + + Future _load({bool reset = false}) async { + if (reset) { + _page = 1; + _hasMore = true; + _items = []; + setState(() => _state = MultiState.loading); + } + if (!_hasMore || _loadingMore) return; + _loadingMore = true; + try { + final BaseResponse> resp = + await ref.read(userApiProvider).amountList( + page: _page, + size: 20, + ); + if (!mounted) return; + if (!resp.isSuccess) { + if (reset) setState(() => _state = MultiState.error); + return; + } + final list = (resp.data?.list as List?) + ?.map((e) => AmountItem.fromJson( + e as Map, + )) + .toList() ?? + const []; + _items.addAll(list); + _hasMore = (resp.data?.hasMore ?? false) && list.length >= 20; + _page++; + setState(() { + _state = _items.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted && reset) setState(() => _state = MultiState.error); + } finally { + _loadingMore = false; + } + } + + @override + Widget build(BuildContext context) { + final fmt = DateFormat('yyyy-MM-dd HH:mm'); + return Scaffold( + appBar: AppBar(title: const Text('金额明细')), + body: RefreshIndicator( + onRefresh: () => _load(reset: true), + child: MultiStateView( + state: _state, + emptyText: '暂无明细', + onRetry: () => _load(reset: true), + child: ListView.separated( + controller: _scroll, + padding: const EdgeInsets.all(12), + itemCount: _items.length + (_hasMore ? 1 : 0), + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) { + if (i >= _items.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + } + final it = _items[i]; + final isIncome = it.type == 'income'; + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: + (isIncome ? Colors.green : Colors.red).withValues(alpha: 0.1), + child: Icon( + isIncome ? Icons.add : Icons.remove, + color: isIncome ? Colors.green : Colors.red, + ), + ), + title: Text(it.title), + subtitle: Text( + '${it.orderNo ?? "—"}\n${fmt.format(it.createdAt)}', + style: const TextStyle(fontSize: 12), + ), + isThreeLine: true, + trailing: Text( + '${isIncome ? "+" : "-"}¥${it.amount.toStringAsFixed(2)}', + style: TextStyle( + color: isIncome ? Colors.green : Colors.red, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/me/authentication_page.dart b/lib/pages/me/authentication_page.dart new file mode 100644 index 0000000..1e1e0fc --- /dev/null +++ b/lib/pages/me/authentication_page.dart @@ -0,0 +1,296 @@ +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 认证页(对应 Android AuthenticationActivity) +/// 三种:个人 / 企业 / 司机 +class AuthenticationPage extends ConsumerStatefulWidget { + const AuthenticationPage({super.key}); + + @override + ConsumerState createState() => + _AuthenticationPageState(); +} + +class _AuthenticationPageState extends ConsumerState + with SingleTickerProviderStateMixin { + late final TabController _tab; + + @override + void initState() { + super.initState(); + _tab = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _tab.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('认证'), + bottom: TabBar( + controller: _tab, + tabs: const [ + Tab(text: '个人'), + Tab(text: '企业'), + Tab(text: '司机'), + ], + ), + ), + body: TabBarView( + controller: _tab, + children: const [ + _PersonalForm(), + _EnterpriseForm(), + _DriverForm(), + ], + ), + ); + } +} + +class _PersonalForm extends ConsumerStatefulWidget { + const _PersonalForm(); + @override + ConsumerState<_PersonalForm> createState() => _PersonalFormState(); +} + +class _PersonalFormState extends ConsumerState<_PersonalForm> { + final _name = TextEditingController(); + final _idCard = TextEditingController(); + final _front = TextEditingController(); + final _back = TextEditingController(); + bool _loading = false; + + @override + void dispose() { + _name.dispose(); + _idCard.dispose(); + _front.dispose(); + _back.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_name.text.isEmpty || _idCard.text.isEmpty) { + Toast.error('请填写姓名和身份证号'); + return; + } + if (_front.text.isEmpty || _back.text.isEmpty) { + Toast.error('请上传身份证正反面'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '提交中…'); + try { + final r = await ref.read(userApiProvider).authPersonal( + name: _name.text.trim(), + idCard: _idCard.text.trim(), + idCardFront: _front.text.trim(), + idCardBack: _back.text.trim(), + ); + EasyLoading.dismiss(); + if (!r.isSuccess) { + Toast.error(r.message ?? '提交失败'); + return; + } + Toast.success('已提交,请等待审核'); + if (mounted) Navigator.pop(context); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) => _form(children: [ + _tf(_name, '真实姓名'), + _tf(_idCard, '身份证号'), + _imgField(_front, '身份证正面 URL(七牛)'), + _imgField(_back, '身份证反面 URL(七牛)'), + _submitBtn('提交个人认证', _loading, _submit), + ]); +} + +class _EnterpriseForm extends ConsumerStatefulWidget { + const _EnterpriseForm(); + @override + ConsumerState<_EnterpriseForm> createState() => _EnterpriseFormState(); +} + +class _EnterpriseFormState extends ConsumerState<_EnterpriseForm> { + final _company = TextEditingController(); + final _license = TextEditingController(); + final _licenseImg = TextEditingController(); + final _legalName = TextEditingController(); + final _legalId = TextEditingController(); + final _legalFront = TextEditingController(); + final _legalBack = TextEditingController(); + bool _loading = false; + + @override + void dispose() { + _company.dispose(); + _license.dispose(); + _licenseImg.dispose(); + _legalName.dispose(); + _legalId.dispose(); + _legalFront.dispose(); + _legalBack.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_company.text.isEmpty || _license.text.isEmpty) { + Toast.error('请填写公司名和营业执照号'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '提交中…'); + try { + final r = await ref.read(userApiProvider).authEnterprise( + companyName: _company.text.trim(), + licenseNo: _license.text.trim(), + licenseImg: _licenseImg.text.trim(), + legalPerson: _legalName.text.trim(), + legalIdCard: _legalId.text.trim(), + legalIdFront: _legalFront.text.trim(), + legalIdBack: _legalBack.text.trim(), + ); + EasyLoading.dismiss(); + if (!r.isSuccess) { + Toast.error(r.message ?? '提交失败'); + return; + } + Toast.success('已提交,请等待审核'); + if (mounted) Navigator.pop(context); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) => _form(children: [ + _tf(_company, '公司名称'), + _tf(_license, '营业执照号'), + _imgField(_licenseImg, '营业执照图片 URL'), + const Divider(), + _tf(_legalName, '法人姓名'), + _tf(_legalId, '法人身份证号'), + _imgField(_legalFront, '法人身份证正面 URL'), + _imgField(_legalBack, '法人身份证反面 URL'), + _submitBtn('提交企业认证', _loading, _submit), + ]); +} + +class _DriverForm extends ConsumerStatefulWidget { + const _DriverForm(); + @override + ConsumerState<_DriverForm> createState() => _DriverFormState(); +} + +class _DriverFormState extends ConsumerState<_DriverForm> { + final _driverNo = TextEditingController(); + final _driverLicense = TextEditingController(); + final _front = TextEditingController(); + final _back = TextEditingController(); + bool _loading = false; + + @override + void dispose() { + _driverNo.dispose(); + _driverLicense.dispose(); + _front.dispose(); + _back.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_driverNo.text.isEmpty || _driverLicense.text.isEmpty) { + Toast.error('请填写驾驶证号和驾驶证'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '提交中…'); + try { + final r = await ref.read(userApiProvider).authDriver( + driverNo: _driverNo.text.trim(), + driverLicense: _driverLicense.text.trim(), + drivingLicenseFront: _front.text.trim(), + drivingLicenseBack: _back.text.trim(), + ); + EasyLoading.dismiss(); + if (!r.isSuccess) { + Toast.error(r.message ?? '提交失败'); + return; + } + Toast.success('已提交,请等待审核'); + if (mounted) Navigator.pop(context); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) => _form(children: [ + _tf(_driverNo, '驾驶证号'), + _tf(_driverLicense, '驾驶证档案编号'), + _imgField(_front, '驾驶证正面 URL'), + _imgField(_back, '驾驶证反面 URL'), + _submitBtn('提交司机认证', _loading, _submit), + ]); +} + +Widget _form({required List children}) => SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: children), + ); + +Widget _tf(TextEditingController c, String label) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: c, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + ), + ); + +Widget _imgField(TextEditingController c, String label) => Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: c, + decoration: InputDecoration( + labelText: label, + hintText: '七牛上传后返回的 URL', + border: const OutlineInputBorder(), + ), + ), + ); + +Widget _submitBtn(String text, bool loading, VoidCallback onTap) => SizedBox( + height: 48, + child: ElevatedButton( + onPressed: loading ? null : onTap, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: Text(loading ? '提交中…' : text), + ), + ); diff --git a/lib/pages/me/feedback_page.dart b/lib/pages/me/feedback_page.dart new file mode 100644 index 0000000..58cddff --- /dev/null +++ b/lib/pages/me/feedback_page.dart @@ -0,0 +1,122 @@ +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/photo_grid_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 反馈页(对应 Android FeedbackActivity) +class FeedbackPage extends ConsumerStatefulWidget { + const FeedbackPage({super.key}); + + @override + ConsumerState createState() => _FeedbackPageState(); +} + +class _FeedbackPageState extends ConsumerState { + final _content = TextEditingController(); + final _contact = TextEditingController(); + final List _images = []; + bool _submitting = false; + + @override + void dispose() { + _content.dispose(); + _contact.dispose(); + super.dispose(); + } + + Future _pickImages() async { + final files = await AlbumPicker.pick(max: 4); + if (files.isEmpty) return; + setState(() => _images.addAll(files)); + } + + Future _submit() async { + if (_content.text.trim().isEmpty) { + Toast.error('请填写问题描述'); + return; + } + setState(() => _submitting = true); + EasyLoading.show(status: '提交中…'); + try { + List? urls; + if (_images.isNotEmpty) { + urls = await ref + .read(uploadServiceProvider) + .uploadBatch(_images); + } + final r = await ref.read(userApiProvider).submitFeedback( + content: _content.text.trim(), + contact: _contact.text.trim().isEmpty + ? null + : _contact.text.trim(), + images: urls, + ); + EasyLoading.dismiss(); + if (!r.isSuccess) { + Toast.error(r.message ?? '提交失败'); + return; + } + Toast.success('反馈已提交'); + if (mounted) Navigator.pop(context); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('问题反馈')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: _content, + maxLines: 6, + decoration: const InputDecoration( + hintText: '请描述您遇到的问题…', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _contact, + decoration: const InputDecoration( + labelText: '联系方式(选填)', + hintText: '手机号/微信号', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + const Text('截图(选填)', + style: TextStyle(color: Colors.grey)), + const SizedBox(height: 8), + PhotoThumbStrip( + paths: _images, + onAdd: _pickImages, + onRemove: (i) => setState(() => _images.removeAt(i)), + ), + const SizedBox(height: 24), + SizedBox( + height: 48, + child: ElevatedButton( + onPressed: _submitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text(_submitting ? '提交中…' : '提交反馈'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/me/me_page.dart b/lib/pages/me/me_page.dart new file mode 100644 index 0000000..f318eb9 --- /dev/null +++ b/lib/pages/me/me_page.dart @@ -0,0 +1,328 @@ +import 'package:autosos_flutter/api/_api_client.dart'; +import 'package:autosos_flutter/model/user.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 我的中心(对应 Android MeFragment) +class MePage extends ConsumerStatefulWidget { + const MePage({super.key}); + + @override + ConsumerState createState() => _MePageState(); +} + +class _MePageState extends ConsumerState { + MultiState _state = MultiState.loading; + UserInfo? _user; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final resp = await ref.read(userApiProvider).info(); + if (!mounted) return; + if (!resp.isSuccess || resp.data == null) { + setState(() => _state = MultiState.error); + return; + } + ref.read(currentUserProvider.notifier).state = resp.data; + setState(() { + _user = resp.data; + _state = MultiState.success; + }); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + Future _logout() async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('退出登录'), + content: const Text('确定要退出当前账号吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Navigator.pop(ctx, true), + child: const Text('退出'), + ), + ], + ), + ); + if (ok != true) return; + EasyLoading.show(status: '退出中…'); + try { + await ref.read(userApiProvider).logout(); + } catch (_) {} + // 清 token + sp + SPUtil().remove('accessToken'); + SPUtil().remove('user'); + ApiClient.instance.clearToken(); + ref.read(accessTokenProvider.notifier).state = null; + ref.read(currentUserProvider.notifier).state = null; + EasyLoading.dismiss(); + if (mounted) context.go(Routes.login); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade100, + body: RefreshIndicator( + onRefresh: _load, + child: MultiStateView( + state: _state, + onRetry: _load, + child: _user == null + ? const SizedBox.shrink() + : ListView( + children: [ + _header(_user!), + const SizedBox(height: 8), + _statsRow(_user!), + const SizedBox(height: 8), + _menu(), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton.icon( + onPressed: _logout, + icon: const Icon(Icons.logout, color: Colors.red), + label: const Text( + '退出登录', + style: TextStyle(color: Colors.red), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + minimumSize: const Size.fromHeight(44), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _header(UserInfo u) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 36, 16, 16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.red.shade400, Colors.red.shade700], + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 32, + backgroundColor: Colors.white, + child: u.avatar != null && u.avatar!.isNotEmpty + ? ClipOval( + child: CachedNetworkImage( + imageUrl: u.avatar!, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ) + : Text( + (u.realName?.isNotEmpty == true + ? u.realName![0] + : (u.username?.isNotEmpty == true ? u.username![0] : '?')) + .toUpperCase(), + style: const TextStyle( + fontSize: 24, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + u.realName ?? u.username ?? '—', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Text(u.phone ?? '—', + style: const TextStyle(color: Colors.white70)), + const SizedBox(width: 8), + _badge(u.authStatus ?? 'none'), + ], + ), + if (u.company != null && u.company!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text(u.company!, + style: const TextStyle(color: Colors.white70)), + ], + ], + ), + ), + IconButton( + onPressed: () => context.push(Routes.setting), + icon: const Icon(Icons.settings, color: Colors.white), + ), + ], + ), + ); + } + + Widget _badge(String status) { + Color bg; + String text; + switch (status) { + case 'personal': + bg = Colors.blue; + text = '个人认证'; + break; + case 'enterprise': + bg = Colors.purple; + text = '企业认证'; + break; + case 'driver': + bg = Colors.orange; + text = '司机认证'; + break; + default: + bg = Colors.grey; + text = '未认证'; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(4), + ), + child: Text(text, + style: const TextStyle(color: Colors.white, fontSize: 11)), + ); + } + + Widget _statsRow(UserInfo u) { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + _stat('余额', '¥${u.balance?.toStringAsFixed(2) ?? "0.00"}'), + _divider(), + _stat('今日订单', '${u.todayOrders ?? 0}'), + _divider(), + _stat('累计订单', '${u.totalOrders ?? 0}'), + ], + ), + ); + } + + Widget _stat(String label, String value) { + return Expanded( + child: Column( + children: [ + Text(value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red, + )), + const SizedBox(height: 2), + Text(label, + style: const TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ); + } + + Widget _divider() => + Container(width: 1, height: 30, color: Colors.grey.shade300); + + Widget _menu() { + return Container( + color: Colors.white, + child: Column( + children: [ + _menuItem(Icons.account_balance_wallet, '提现', () { + context.push(Routes.withdrawal); + }), + _menuItem(Icons.history, '提现记录', () { + context.push(Routes.withdrawalRecord); + }), + _menuItem(Icons.payment, '充值', () { + context.push(Routes.recharge); + }), + _menuItem(Icons.list_alt, '金额明细', () { + context.push(Routes.amountList); + }), + _dividerLine(), + _menuItem(Icons.edit, '完善资料', () { + context.push(Routes.perfect); + }), + _menuItem(Icons.verified_user, '认证', () { + context.push(Routes.auth); + }), + _dividerLine(), + _menuItem(Icons.message, '消息中心', () { + context.push(Routes.messenger); + }), + _menuItem(Icons.bug_report, '问题反馈', () { + context.push(Routes.feedback); + }), + _menuItem(Icons.tune, '性能优化', () { + context.push(Routes.optimization); + }), + ], + ), + ); + } + + Widget _menuItem(IconData icon, String label, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Icon(icon, color: Colors.red, size: 22), + const SizedBox(width: 12), + Expanded(child: Text(label, style: const TextStyle(fontSize: 15))), + const Icon(Icons.chevron_right, color: Colors.grey), + ], + ), + ), + ); + } + + Widget _dividerLine() => Divider( + height: 1, + thickness: 1, + color: Colors.grey.shade200, + indent: 16, + ); +} diff --git a/lib/pages/me/messenger_page.dart b/lib/pages/me/messenger_page.dart new file mode 100644 index 0000000..e24b8ea --- /dev/null +++ b/lib/pages/me/messenger_page.dart @@ -0,0 +1,166 @@ +import 'package:autosos_flutter/model/misc.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +/// 消息中心(对应 Android MessengerActivity) +class MessengerPage extends ConsumerStatefulWidget { + const MessengerPage({super.key}); + + @override + ConsumerState createState() => _MessengerPageState(); +} + +class _MessengerPageState extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + int _page = 1; + bool _hasMore = true; + bool _loadingMore = false; + final _scroll = ScrollController(); + + @override + void initState() { + super.initState(); + _load(reset: true); + _scroll.addListener(() { + if (_scroll.position.pixels >= + _scroll.position.maxScrollExtent - 200) { + _load(); + } + }); + } + + @override + void dispose() { + _scroll.dispose(); + super.dispose(); + } + + Future _load({bool reset = false}) async { + if (reset) { + _page = 1; + _hasMore = true; + _items = []; + setState(() => _state = MultiState.loading); + } + if (!_hasMore || _loadingMore) return; + _loadingMore = true; + try { + final resp = await ref.read(userApiProvider).messages( + page: _page, + size: 20, + ); + if (!mounted) return; + if (!resp.isSuccess) { + if (reset) setState(() => _state = MultiState.error); + return; + } + final list = resp.data?.list ?? []; + _items.addAll(list); + _hasMore = (resp.data?.hasMore ?? false) && list.length >= 20; + _page++; + setState(() { + _state = _items.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted && reset) setState(() => _state = MultiState.error); + } finally { + _loadingMore = false; + } + } + + Future _onTap(MessengerItem item) async { + if (!item.read) { + try { + await ref.read(userApiProvider).readMessage(item.id); + } catch (_) {} + } + if (!mounted) return; + setState(() { + final i = _items.indexWhere((e) => e.id == item.id); + if (i >= 0) { + _items[i] = MessengerItem( + id: item.id, + title: item.title, + content: item.content, + createdAt: item.createdAt, + read: true, + link: item.link, + ); + } + }); + if (item.link != null && item.link!.isNotEmpty) { + context.push( + '${Routes.webview}?url=${Uri.encodeComponent(item.link!)}&title=${Uri.encodeComponent(item.title)}', + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('消息中心')), + body: RefreshIndicator( + onRefresh: () => _load(reset: true), + child: MultiStateView( + state: _state, + emptyText: '暂无消息', + onRetry: () => _load(reset: true), + child: ListView.separated( + controller: _scroll, + itemCount: _items.length + (_hasMore ? 1 : 0), + separatorBuilder: (_, __) => Divider( + height: 1, + color: Colors.grey.shade200, + indent: 16, + ), + itemBuilder: (ctx, i) { + if (i >= _items.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + } + final m = _items[i]; + return ListTile( + leading: CircleAvatar( + backgroundColor: m.read + ? Colors.grey.shade300 + : Theme.of(context).primaryColor, + child: const Icon(Icons.notifications, + color: Colors.white, size: 20), + ), + title: Text( + m.title, + style: TextStyle( + fontWeight: m.read + ? FontWeight.normal + : FontWeight.bold, + ), + ), + subtitle: Text( + m.content, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 13), + ), + trailing: Text( + DateFormat('MM-dd HH:mm').format(m.createdAt), + style: const TextStyle(fontSize: 11, color: Colors.grey), + ), + onTap: () => _onTap(m), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/me/optimization_page.dart b/lib/pages/me/optimization_page.dart new file mode 100644 index 0000000..2d95aeb --- /dev/null +++ b/lib/pages/me/optimization_page.dart @@ -0,0 +1,185 @@ +import 'package:autosos_flutter/service/background_service.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:autosos_flutter/service/push_service.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 性能优化页(对应 Android OptimizationActivity) +/// 检查并展示:定位 / 推送 / 后台 / 心跳 / 网络 各模块状态 +class OptimizationPage extends ConsumerStatefulWidget { + const OptimizationPage({super.key}); + + @override + ConsumerState createState() => _OptimizationPageState(); +} + +class _OptimizationPageState extends ConsumerState { + bool _locating = false; + String? _lastLoc; + String? _cid; + bool _bgRunning = false; + bool _heartbeat = false; + int _online = 0; + bool _testing = false; + String? _netResult; + + @override + void initState() { + super.initState(); + _refresh(); + } + + Future _refresh() async { + final loc = LocationService.instance.last; + final bg = await BackgroundService.instance.isRunning(); + setState(() { + _cid = PushService.instance.clientId; + _bgRunning = bg; + _lastLoc = loc == null + ? null + : '(${(loc['latitude'] as num? ?? 0).toDouble().toStringAsFixed(4)}, ${(loc['longitude'] as num? ?? 0).toDouble().toStringAsFixed(4)})'; + }); + } + + Future _fetchOnce() async { + setState(() => _locating = true); + try { + final loc = await LocationService.instance.fetchOnce(); + if (loc == null) { + Toast.error('定位失败'); + } else { + setState(() { + _lastLoc = + '(${(loc['latitude'] as num? ?? 0).toDouble().toStringAsFixed(4)}, ${(loc['longitude'] as num? ?? 0).toDouble().toStringAsFixed(4)})'; + }); + } + } catch (e) { + Toast.error('定位异常:$e'); + } finally { + if (mounted) setState(() => _locating = false); + } + } + + Future _testNet() async { + setState(() { + _testing = true; + _netResult = null; + }); + final sw = Stopwatch()..start(); + try { + final r = await LocationService.instance.fetchOnce(); + sw.stop(); + if (r != null) { + setState(() { + _netResult = '定位耗时 ${sw.elapsedMilliseconds} ms'; + }); + Toast.success('网络正常'); + } else { + setState(() { + _netResult = '定位失败'; + }); + } + } catch (e) { + sw.stop(); + setState(() { + _netResult = '异常:$e'; + }); + } finally { + if (mounted) setState(() => _testing = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('性能优化')), + body: ListView( + children: [ + _section('定位服务'), + ListTile( + leading: const Icon(Icons.location_on, color: Colors.red), + title: const Text('当前位置'), + subtitle: Text(_lastLoc ?? '尚未获取'), + trailing: _locating + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onTap: _fetchOnce, + ), + _section('推送服务'), + ListTile( + leading: const Icon(Icons.notifications, color: Colors.red), + title: const Text('个推 CID'), + subtitle: Text(_cid ?? '未初始化'), + ), + _section('后台保活'), + ListTile( + leading: Icon( + Icons.alarm_on, + color: _bgRunning ? Colors.green : Colors.grey, + ), + title: const Text('Background Service'), + subtitle: Text(_bgRunning ? '运行中' : '未运行'), + trailing: TextButton( + onPressed: () async { + if (_bgRunning) { + await BackgroundService.instance.stop(); + } else { + await BackgroundService.instance.start(); + } + _refresh(); + }, + child: Text(_bgRunning ? '停止' : '启动'), + ), + ), + _section('心跳 / 网络'), + ListTile( + leading: const Icon(Icons.favorite, color: Colors.red), + title: const Text('心跳'), + subtitle: Text(_heartbeat ? '正常' : '—'), + trailing: Text('${_online}s'), + ), + ListTile( + leading: const Icon(Icons.network_check, color: Colors.red), + title: const Text('网络测试'), + subtitle: Text(_netResult ?? '点击测试'), + trailing: _testing + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.play_arrow), + onTap: _testing ? null : _testNet, + ), + _section('开发者'), + ListTile( + title: const Text('强制重连推送'), + onTap: () async { + PushService.instance.init(); + _refresh(); + Toast.success('已重连'); + }, + ), + ], + ), + ); + } + + Widget _section(String title) => Container( + color: Colors.grey.shade100, + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + width: double.infinity, + child: Text( + title, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ); +} diff --git a/lib/pages/me/perfect_page.dart b/lib/pages/me/perfect_page.dart new file mode 100644 index 0000000..cbb06b3 --- /dev/null +++ b/lib/pages/me/perfect_page.dart @@ -0,0 +1,171 @@ +import 'package:autosos_flutter/model/user.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 完善资料页(对应 Android MePerfectActivity) +class PerfectPage extends ConsumerStatefulWidget { + const PerfectPage({super.key}); + + @override + ConsumerState createState() => _PerfectPageState(); +} + +class _PerfectPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _realName = TextEditingController(); + final _idCard = TextEditingController(); + final _phone = TextEditingController(); + final _address = TextEditingController(); + final _email = TextEditingController(); + final _company = TextEditingController(); + bool _loading = false; + UserInfo? _user; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void dispose() { + _realName.dispose(); + _idCard.dispose(); + _phone.dispose(); + _address.dispose(); + _email.dispose(); + _company.dispose(); + super.dispose(); + } + + Future _load() async { + final resp = await ref.read(userApiProvider).info(); + if (!mounted || !resp.isSuccess) return; + final u = resp.data!; + setState(() { + _user = u; + _realName.text = u.realName ?? ''; + _idCard.text = u.idCard ?? ''; + _phone.text = u.phone ?? ''; + _address.text = u.address ?? ''; + _email.text = u.email ?? ''; + _company.text = u.company ?? ''; + }); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _loading = true); + EasyLoading.show(status: '保存中…'); + try { + final resp = await ref.read(userApiProvider).perfectInfo({ + 'real_name': _realName.text.trim(), + 'id_card': _idCard.text.trim(), + 'phone': _phone.text.trim(), + 'address': _address.text.trim(), + 'email': _email.text.trim(), + 'company': _company.text.trim(), + }); + EasyLoading.dismiss(); + if (!resp.isSuccess) { + Toast.error(resp.message ?? '保存失败'); + return; + } + ref.read(currentUserProvider.notifier).state = resp.data; + Toast.success('保存成功'); + if (mounted) Navigator.pop(context); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('保存失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('完善资料')), + body: _user == null + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _realName, + decoration: const InputDecoration( + labelText: '真实姓名', + border: OutlineInputBorder(), + ), + validator: (v) => + (v == null || v.isEmpty) ? '请输入姓名' : null, + ), + const SizedBox(height: 12), + TextFormField( + controller: _idCard, + decoration: const InputDecoration( + labelText: '身份证号', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _phone, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: '联系电话', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _email, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration( + labelText: '邮箱', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _company, + decoration: const InputDecoration( + labelText: '所属公司(选填)', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextFormField( + controller: _address, + maxLines: 2, + decoration: const InputDecoration( + labelText: '常住地址', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _loading ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).primaryColor, + minimumSize: const Size.fromHeight(48), + ), + child: Text(_loading ? '保存中…' : '保存'), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/me/recharge_page.dart b/lib/pages/me/recharge_page.dart new file mode 100644 index 0000000..e339c42 --- /dev/null +++ b/lib/pages/me/recharge_page.dart @@ -0,0 +1,154 @@ +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/qr_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluwx/fluwx.dart'; + +/// 充值页(对应 Android RechargeActivity) +class RechargePage extends ConsumerStatefulWidget { + const RechargePage({super.key}); + + @override + ConsumerState createState() => _RechargePageState(); +} + +class _RechargePageState extends ConsumerState { + final _amount = TextEditingController(); + String _payMethod = 'wx'; + String? _qrUrl; + bool _loading = false; + + final _presets = [50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0]; + + @override + void dispose() { + _amount.dispose(); + super.dispose(); + } + + Future _submit() async { + final amount = double.tryParse(_amount.text.trim()); + if (amount == null || amount <= 0) { + Toast.error('请输入充值金额'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '生成订单中…'); + try { + final r = await ref.read(userApiProvider).recharge( + amount: amount, + payMethod: _payMethod, + ); + EasyLoading.dismiss(); + if (!r.isSuccess || r.data == null) { + Toast.error(r.message ?? '下单失败'); + return; + } + // 微信支付(带 sign 的预支付) + if (_payMethod == 'wx' && r.data is Map) { + final m = r.data as Map; + await Fluwx().pay( + which: Payment( + appId: m['appid'] ?? '', + partnerId: m['partnerid'] ?? '', + prepayId: m['prepayid'] ?? '', + packageValue: m['package'] ?? 'Sign=WXPay', + nonceStr: m['noncestr'] ?? '', + timestamp: int.tryParse(m['timestamp']?.toString() ?? '') ?? 0, + sign: m['sign'] ?? '', + ), + ); + return; + } + // 二维码 + if (r.data is Map && (r.data as Map)['qr_code_url'] != null) { + setState(() => _qrUrl = (r.data as Map)['qr_code_url']); + } else { + Toast.success('订单已生成'); + } + } catch (e) { + EasyLoading.dismiss(); + Toast.error('充值失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('充值')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('选择充值金额', + style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: _presets + .map( + (a) => ChoiceChip( + label: Text('¥${a.toStringAsFixed(0)}'), + selected: _amount.text == a.toStringAsFixed(0), + onSelected: (_) => + setState(() => _amount.text = a.toStringAsFixed(0)), + ), + ) + .toList(), + ), + const SizedBox(height: 12), + TextField( + controller: _amount, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), + ], + decoration: const InputDecoration( + labelText: '其他金额', + prefixText: '¥ ', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + const Text('支付方式', + style: TextStyle(fontWeight: FontWeight.bold)), + RadioListTile( + title: const Text('微信支付'), + value: 'wx', + groupValue: _payMethod, + onChanged: (v) => setState(() => _payMethod = v ?? 'wx'), + ), + if (_qrUrl != null) ...[ + const SizedBox(height: 16), + Center( + child: QrImageView(data: _qrUrl!, size: 200), + ), + const SizedBox(height: 8), + const Center( + child: Text('请使用微信扫码支付', + style: TextStyle(color: Colors.grey)), + ), + ], + const SizedBox(height: 24), + ElevatedButton( + onPressed: _loading ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + minimumSize: const Size.fromHeight(48), + ), + child: Text(_loading ? '处理中…' : '立即充值'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/me/setting_page.dart b/lib/pages/me/setting_page.dart new file mode 100644 index 0000000..c1aecbd --- /dev/null +++ b/lib/pages/me/setting_page.dart @@ -0,0 +1,282 @@ +import 'package:autosos_flutter/api/_api_client.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/service/audio_service.dart'; +import 'package:autosos_flutter/service/background_service.dart'; +import 'package:autosos_flutter/service/push_service.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +/// 设置页(对应 Android MeSettingActivity) +class SettingPage extends ConsumerStatefulWidget { + const SettingPage({super.key}); + + @override + ConsumerState createState() => _SettingPageState(); +} + +class _SettingPageState extends ConsumerState { + bool _push = true; + bool _sound = true; + bool _bg = true; + String _version = ''; + String _cid = ''; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + _push = (SPUtil().get('setting.push') as bool?) ?? true; + _sound = (SPUtil().get('setting.sound') as bool?) ?? true; + _bg = (SPUtil().get('setting.bg') as bool?) ?? true; + final info = await PackageInfo.fromPlatform(); + _cid = PushService.instance.clientId ?? ''; + if (!mounted) return; + setState(() { + _version = '${info.version} (${info.buildNumber})'; + }); + } + + Future _save() async { + SPUtil().setBool('setting.push', _push); + SPUtil().setBool('setting.sound', _sound); + SPUtil().setBool('setting.bg', _bg); + AudioService.instance.enableSound = _sound; + if (_bg) { + await BackgroundService.instance.start(); + } else { + await BackgroundService.instance.stop(); + } + if (!_push) { + PushService.instance.turnOff(); + } + } + + Future _clearCache() async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('清除缓存'), + content: const Text('将清除图片缓存和临时文件,确定吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('清除'), + ), + ], + ), + ); + if (ok != true) return; + EasyLoading.show(status: '清除中…'); + // Flutter 没有原生 clear cache API,这里仅做演示 + await Future.delayed(const Duration(seconds: 1)); + EasyLoading.dismiss(); + Toast.success('缓存已清除'); + } + + Future _checkUpdate() async { + EasyLoading.show(status: '检查中…'); + try { + final r = await ref.read(userApiProvider).checkUpdate(); + EasyLoading.dismiss(); + if (!r.isSuccess || r.data == null) { + Toast.error('检查失败'); + return; + } + final u = r.data!; + if (u.latestVersion.isEmpty) { + Toast.success('已是最新版本'); + return; + } + if (!mounted) return; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text('发现新版本 v${u.latestVersion}'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (u.changelog != null && u.changelog!.isNotEmpty) + Text(u.changelog!, + style: const TextStyle(fontSize: 13)), + if (u.forceUpdate) ...[ + const SizedBox(height: 8), + const Text('⚠️ 此版本必须升级', + style: TextStyle(color: Colors.red)), + ], + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('稍后'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(ctx); + context.push( + '${Routes.webview}?url=${Uri.encodeComponent(u.downloadUrl ?? "")}&title=版本更新', + ); + }, + child: const Text('立即升级'), + ), + ], + ), + ); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('检查失败:$e'); + } + } + + Future _logout() async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('退出登录'), + content: const Text('确定要退出当前账号吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Navigator.pop(ctx, true), + child: const Text('退出'), + ), + ], + ), + ); + if (ok != true) return; + try { + await ref.read(userApiProvider).logout(); + } catch (_) {} + SPUtil().remove('accessToken'); + SPUtil().remove('user'); + ApiClient.instance.clearToken(); + ref.read(accessTokenProvider.notifier).state = null; + if (mounted) context.go(Routes.login); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('设置')), + body: ListView( + children: [ + _section('消息通知'), + SwitchListTile( + title: const Text('推送通知'), + subtitle: const Text('接收订单推送'), + value: _push, + onChanged: (v) { + setState(() => _push = v); + _save(); + }, + ), + SwitchListTile( + title: const Text('声音提醒'), + subtitle: const Text('新订单播报'), + value: _sound, + onChanged: (v) { + setState(() => _sound = v); + _save(); + }, + ), + _section('服务设置'), + SwitchListTile( + title: const Text('后台保活'), + subtitle: const Text('App 切到后台后保持心跳'), + value: _bg, + onChanged: (v) { + setState(() => _bg = v); + _save(); + }, + ), + ListTile( + title: const Text('个推 CID'), + subtitle: Text(_cid.isEmpty ? '未获取到' : _cid, + style: const TextStyle(fontSize: 12)), + trailing: const Icon(Icons.copy), + onTap: () { + if (_cid.isNotEmpty) { + // ignore: deprecated_member_use + // Clipboard.setData(ClipboardData(text: _cid)); + Toast.success('CID 已复制'); + } + }, + ), + _section('其他'), + ListTile( + title: const Text('清除缓存'), + trailing: const Icon(Icons.chevron_right), + onTap: _clearCache, + ), + ListTile( + title: const Text('检查更新'), + subtitle: Text('当前版本 $_version'), + trailing: const Icon(Icons.chevron_right), + onTap: _checkUpdate, + ), + ListTile( + title: const Text('性能优化'), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.push(Routes.optimization), + ), + ListTile( + title: const Text('用户协议'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + context.push( + '${Routes.webview}?url=${Uri.encodeComponent(Constant.workOrderUrl)}&title=用户协议', + ); + }, + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton.icon( + onPressed: _logout, + icon: const Icon(Icons.logout, color: Colors.red), + label: const Text('退出登录', + style: TextStyle(color: Colors.red)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + minimumSize: const Size.fromHeight(44), + ), + ), + ), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _section(String title) => Container( + width: double.infinity, + color: Colors.grey.shade100, + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + title, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ); +} diff --git a/lib/pages/me/withdrawal_page.dart b/lib/pages/me/withdrawal_page.dart new file mode 100644 index 0000000..8c8fb8d --- /dev/null +++ b/lib/pages/me/withdrawal_page.dart @@ -0,0 +1,190 @@ +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 提现申请页(对应 Android TiXianActivity) +class WithdrawalPage extends ConsumerStatefulWidget { + const WithdrawalPage({super.key}); + + @override + ConsumerState createState() => _WithdrawalPageState(); +} + +class _WithdrawalPageState extends ConsumerState { + final _amount = TextEditingController(); + final _realName = TextEditingController(); + final _bankAccount = TextEditingController(); + bool _loading = false; + double _balance = 0; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void dispose() { + _amount.dispose(); + _realName.dispose(); + _bankAccount.dispose(); + super.dispose(); + } + + Future _load() async { + final resp = await ref.read(userApiProvider).info(); + if (!mounted || !resp.isSuccess || resp.data == null) return; + final u = resp.data!; + setState(() { + _balance = u.balance ?? 0; + _realName.text = u.realName ?? ''; + }); + } + + Future _submit() async { + final amount = double.tryParse(_amount.text.trim()); + if (amount == null || amount <= 0) { + Toast.error('请输入提现金额'); + return; + } + if (amount > _balance) { + Toast.error('提现金额不能超过余额'); + return; + } + if (_realName.text.isEmpty) { + Toast.error('请填写收款人姓名'); + return; + } + if (_bankAccount.text.isEmpty) { + Toast.error('请填写银行卡号'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '提交中…'); + try { + final r = await ref.read(userApiProvider).applyWithdrawal( + amount: amount, + bankAccount: _bankAccount.text.trim(), + realName: _realName.text.trim(), + ); + EasyLoading.dismiss(); + if (!r.isSuccess) { + Toast.error(r.message ?? '提现失败'); + return; + } + Toast.success('提现申请已提交'); + if (mounted) context.pop(true); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提现异常:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('提现')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.red.shade400, Colors.red.shade700], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '可提现余额(元)', + style: TextStyle(color: Colors.white70), + ), + const SizedBox(height: 4), + Text( + '¥${_balance.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + TextField( + controller: _amount, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), + ], + decoration: const InputDecoration( + labelText: '提现金额', + prefixText: '¥ ', + border: OutlineInputBorder(), + ), + style: const TextStyle( + fontSize: 24, + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => _amount.text = _balance.toStringAsFixed(2), + child: const Text('全部提现'), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _realName, + decoration: const InputDecoration( + labelText: '收款人姓名', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _bankAccount, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: '银行卡号', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: _loading ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + minimumSize: const Size.fromHeight(48), + ), + child: Text(_loading ? '提交中…' : '确认提现'), + ), + const SizedBox(height: 12), + const Text( + '• 提现申请提交后,财务将在 1-3 个工作日内处理\n' + '• 单笔最低 1 元,最高 50000 元\n' + '• 如有问题请联系客服', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/me/withdrawal_record_page.dart b/lib/pages/me/withdrawal_record_page.dart new file mode 100644 index 0000000..8fdfc70 --- /dev/null +++ b/lib/pages/me/withdrawal_record_page.dart @@ -0,0 +1,128 @@ +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +/// 提现记录(对应 Android TiXianListActivity) +class WithdrawalRecordPage extends ConsumerStatefulWidget { + const WithdrawalRecordPage({super.key}); + + @override + ConsumerState createState() => + _WithdrawalRecordPageState(); +} + +class _WithdrawalRecordPageState + extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final BaseResponse> resp = + await ref.read(userApiProvider).withdrawalRecords(); + if (!mounted) return; + if (!resp.isSuccess) { + setState(() => _state = MultiState.error); + return; + } + final list = (resp.data?.list as List?) + ?.map((e) => WithdrawalRecord.fromJson( + e as Map, + )) + .toList() ?? + const []; + setState(() { + _items = list; + _state = list.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + @override + Widget build(BuildContext context) { + final fmt = DateFormat('yyyy-MM-dd HH:mm'); + return Scaffold( + appBar: AppBar(title: const Text('提现记录')), + body: RefreshIndicator( + onRefresh: _load, + child: MultiStateView( + state: _state, + emptyText: '暂无提现记录', + onRetry: _load, + child: ListView.separated( + padding: const EdgeInsets.all(12), + itemCount: _items.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) { + final w = _items[i]; + Color color; + String status; + switch (w.status) { + case 'success': + color = Colors.green; + status = '已到账'; + break; + case 'failed': + color = Colors.red; + status = '已失败'; + break; + default: + color = Colors.orange; + status = '审核中'; + } + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: color.withValues(alpha: 0.1), + child: Icon(Icons.account_balance_wallet, color: color), + ), + title: Text( + '¥${w.amount.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + subtitle: Text( + '${w.bankAccount ?? "—"}\n${fmt.format(w.createdAt)}', + style: const TextStyle(fontSize: 12), + ), + isThreeLine: true, + trailing: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status, + style: TextStyle(color: color, fontSize: 11), + ), + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/pages/order/autograph_page.dart b/lib/pages/order/autograph_page.dart new file mode 100644 index 0000000..fa3f2a4 --- /dev/null +++ b/lib/pages/order/autograph_page.dart @@ -0,0 +1,136 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:signature/signature.dart'; +import 'package:uuid/uuid.dart'; + +/// 客户签字页(对应 Android AutographActivity) +class AutographPage extends ConsumerStatefulWidget { + final int orderId; + const AutographPage({super.key, required this.orderId}); + + @override + ConsumerState createState() => _AutographPageState(); +} + +class _AutographPageState extends ConsumerState { + late final SignatureController _sigCtrl; + bool _submitting = false; + + @override + void initState() { + super.initState(); + _sigCtrl = SignatureController( + penStrokeWidth: 3, + penColor: Colors.black, + exportBackgroundColor: Colors.white, + ); + } + + @override + void dispose() { + _sigCtrl.dispose(); + super.dispose(); + } + + Future _submit() async { + if (_sigCtrl.isEmpty) { + Toast.error('请先签名'); + return; + } + setState(() => _submitting = true); + EasyLoading.show(status: '上传中…'); + try { + // 1) 导出 PNG 字节 + final Uint8List? bytes = await _sigCtrl.toPngBytes(); + if (bytes == null) throw '导出签名失败'; + // 2) 落盘到缓存 + final dir = await getTemporaryDirectory(); + final file = File( + '${dir.path}/sig_${const Uuid().v4()}.png', + ); + await file.writeAsBytes(bytes, flush: true); + // 3) 走七牛 + final url = + await ref.read(uploadServiceProvider).upload(file.path); + // 4) 通知后端 + final resp = await ref + .read(orderApiProvider) + .uploadSignature(widget.orderId, url); + EasyLoading.dismiss(); + if (!resp.isSuccess) { + Toast.error(resp.message ?? '保存失败'); + return; + } + Toast.success('签字已保存'); + if (mounted) context.pop(true); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('上传失败:$e'); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('客户签字'), + actions: [ + TextButton( + onPressed: () => _sigCtrl.clear(), + child: const Text('清除', + style: TextStyle(color: Colors.white)), + ), + ], + ), + body: Column( + children: [ + Container( + color: Colors.amber.shade50, + padding: const EdgeInsets.all(12), + width: double.infinity, + child: const Text( + '请客户在下方手写签字,确认本次救援服务内容与费用', + style: TextStyle(color: Colors.brown, fontSize: 13), + ), + ), + Expanded( + child: Container( + color: Colors.white, + margin: const EdgeInsets.all(12), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Signature( + controller: _sigCtrl, + backgroundColor: Colors.white, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _submitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + minimumSize: const Size.fromHeight(48), + ), + child: Text(_submitting ? '提交中…' : '确认提交签字'), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/order/check_page.dart b/lib/pages/order/check_page.dart new file mode 100644 index 0000000..f866186 --- /dev/null +++ b/lib/pages/order/check_page.dart @@ -0,0 +1,229 @@ +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +import '../../router/app_router.dart'; + +/// 核单页(对应 Android CheckOrderActivity / CheckFragment) +/// 展示待核单订单,技师确认后调 /v2/order/check 进入待接单 +class CheckPage extends ConsumerStatefulWidget { + const CheckPage({super.key}); + + @override + ConsumerState createState() => _CheckPageState(); +} + +class _CheckPageState extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final api = ref.read(orderApiProvider); + final BaseResponse> resp = await api.checkList(); + if (!mounted) return; + if (!resp.isSuccess) { + setState(() => _state = MultiState.error); + Toast.error(resp.message ?? '加载失败'); + return; + } + setState(() { + _items = resp.data ?? []; + _state = _items.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + Future _submit(CheckItem item) async { + final remark = await _showRemarkDialog(); + if (remark == null) return; + EasyLoading.show(status: '提交中…'); + try { + final resp = await ref.read(orderApiProvider).submitCheck( + orderId: item.orderId, + remark: remark, + ); + EasyLoading.dismiss(); + if (!resp.isSuccess) { + Toast.error(resp.message ?? '核单失败'); + return; + } + Toast.success('核单成功'); + setState(() => _items.removeWhere((e) => e.orderId == item.orderId)); + if (_items.isEmpty) setState(() => _state = MultiState.empty); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('核单异常:$e'); + } + } + + Future _showRemarkDialog() async { + final ctrl = TextEditingController(); + return showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('核单备注'), + content: TextField( + controller: ctrl, + maxLines: 3, + decoration: const InputDecoration( + hintText: '请输入核单说明(可选)', + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, ctrl.text.trim()), + child: const Text('确认'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('待核单'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _load), + ], + ), + body: RefreshIndicator( + onRefresh: _load, + child: MultiStateView( + state: _state, + onRetry: _load, + child: ListView.separated( + padding: const EdgeInsets.all(12), + itemCount: _items.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) => _CheckCard( + item: _items[i], + onSubmit: () => _submit(_items[i]), + onDetail: () => context.push( + '${Routes.orderComplete}/${_items[i].orderId}', + ), + ), + ), + ), + ), + ); + } +} + +class _CheckCard extends StatelessWidget { + final CheckItem item; + final VoidCallback onSubmit; + final VoidCallback onDetail; + const _CheckCard({ + required this.item, + required this.onSubmit, + required this.onDetail, + }); + + @override + Widget build(BuildContext context) { + final fmt = DateFormat('MM-dd HH:mm'); + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onDetail, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + item.serviceType, + style: TextStyle( + color: Colors.orange.shade700, + fontSize: 12, + ), + ), + ), + const Spacer(), + Text( + '¥${item.amount.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Text('订单号:${item.orderNo}', + style: const TextStyle(fontSize: 13)), + if (item.address != null && item.address!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text('地点:${item.address}', + style: TextStyle( + fontSize: 13, color: Colors.grey.shade700)), + ], + const SizedBox(height: 4), + Text('时间:${fmt.format(item.createdAt)}', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500)), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: onDetail, + child: const Text('查看详情'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: onSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: const Text('确认核单'), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/order/collection_page.dart b/lib/pages/order/collection_page.dart new file mode 100644 index 0000000..6c71a33 --- /dev/null +++ b/lib/pages/order/collection_page.dart @@ -0,0 +1,248 @@ +import 'package:autosos_flutter/model/base_response.dart'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/qr_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fluwx/fluwx.dart'; +import 'package:go_router/go_router.dart'; + +/// 收款页(对应 Android FinishOrderMoneyActivity) +/// 三种方式:微信支付(技师端唤起收款码让客户扫)/ 现金收款 / 已扫码后回调 +class CollectionPage extends ConsumerStatefulWidget { + final int orderId; + const CollectionPage({super.key, required this.orderId}); + + @override + ConsumerState createState() => _CollectionPageState(); +} + +class _CollectionPageState extends ConsumerState { + Order? _order; + final TextEditingController _amountCtrl = TextEditingController(); + String _qrUrl = ''; + bool _loading = false; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void dispose() { + _amountCtrl.dispose(); + super.dispose(); + } + + Future _load() async { + try { + final resp = + await ref.read(orderApiProvider).orderDetail(widget.orderId); + if (!mounted) return; + if (!resp.isSuccess || resp.data == null) { + Toast.error('订单不存在'); + return; + } + setState(() { + _order = resp.data; + _amountCtrl.text = resp.data!.amount.toStringAsFixed(2); + }); + } catch (e) { + Toast.error('加载失败:$e'); + } + } + + Future _wxPay() async { + final amount = double.tryParse(_amountCtrl.text.trim()); + if (amount == null || amount <= 0) { + Toast.error('请输入有效金额'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '生成中…'); + try { + final BaseResponse resp = await ref + .read(orderApiProvider) + .createPayQrCode(widget.orderId, amount); + if (!resp.isSuccess) { + EasyLoading.dismiss(); + Toast.error(resp.message ?? '生成失败'); + return; + } + setState(() => _qrUrl = resp.data ?? ''); + EasyLoading.dismiss(); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('生成失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + Future _wxPayNative() async { + final amount = double.tryParse(_amountCtrl.text.trim()); + if (amount == null || amount <= 0) { + Toast.error('请输入有效金额'); + return; + } + setState(() => _loading = true); + EasyLoading.show(status: '调起微信…'); + try { + final BaseResponse resp = await ref + .read(orderApiProvider) + .createWxPay(widget.orderId, amount); + EasyLoading.dismiss(); + if (!resp.isSuccess || resp.data == null) { + Toast.error(resp.message ?? '下单失败'); + return; + } + final pay = resp.data!; + await Fluwx().pay( + which: Payment( + appId: pay.appId, + partnerId: pay.partnerId, + prepayId: pay.prepayId, + packageValue: pay.packageValue, + nonceStr: pay.nonceStr, + timestamp: int.tryParse(pay.timeStamp) ?? 0, + sign: pay.sign, + ), + ); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('调起失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + Future _cashPay() async { + final amount = double.tryParse(_amountCtrl.text.trim()); + if (amount == null || amount <= 0) { + Toast.error('请输入有效金额'); + return; + } + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('现金收款确认'), + content: Text('确认已收到客户现金 ¥${amount.toStringAsFixed(2)}?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('确认'), + ), + ], + ), + ); + if (ok != true) return; + setState(() => _loading = true); + try { + final resp = await ref + .read(orderApiProvider) + .confirmCashPay(widget.orderId, amount); + if (!resp.isSuccess) { + Toast.error(resp.message ?? '提交失败'); + return; + } + Toast.success('已记录现金收款'); + if (mounted) context.pop(true); + } catch (e) { + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('客户付款')), + body: _order == null + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + '订单号:${_order!.orderNo}', + style: const TextStyle(fontSize: 13), + ), + const SizedBox(height: 16), + TextField( + controller: _amountCtrl, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: const InputDecoration( + labelText: '收款金额', + prefixText: '¥ ', + border: OutlineInputBorder(), + ), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + const SizedBox(height: 24), + if (_qrUrl.isNotEmpty) ...[ + Center( + child: QrImageView( + data: _qrUrl, + size: 220, + ), + ), + const SizedBox(height: 8), + const Center( + child: Text( + '请客户扫码支付', + style: TextStyle(color: Colors.grey), + ), + ), + const SizedBox(height: 16), + ], + ElevatedButton.icon( + onPressed: _loading ? null : _wxPay, + icon: const Icon(Icons.qr_code_2), + label: const Text('生成收款二维码'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + minimumSize: const Size.fromHeight(48), + ), + ), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _loading ? null : _wxPayNative, + icon: const Icon(Icons.wechat), + label: const Text('调起微信收款'), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.green), + foregroundColor: Colors.green, + minimumSize: const Size.fromHeight(48), + ), + ), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _loading ? null : _cashPay, + icon: const Icon(Icons.money), + label: const Text('现金收款确认'), + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/order/complete_page.dart b/lib/pages/order/complete_page.dart new file mode 100644 index 0000000..56ad23d --- /dev/null +++ b/lib/pages/order/complete_page.dart @@ -0,0 +1,316 @@ +import 'package:autosos_flutter/model/misc.dart'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 完成订单页(对应 Android FinishOrderActivity) +/// 录入费用明细 → 提交完成 → 跳转记录页 +class CompletePage extends ConsumerStatefulWidget { + final int orderId; + const CompletePage({super.key, required this.orderId}); + + @override + ConsumerState createState() => _CompletePageState(); +} + +class _CompletePageState extends ConsumerState { + MultiState _state = MultiState.loading; + Order? _order; + final List _items = []; + final TextEditingController _remarkCtrl = TextEditingController(); + bool _submitting = false; + + @override + void initState() { + super.initState(); + _load(); + } + + @override + void dispose() { + _remarkCtrl.dispose(); + super.dispose(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final resp = await ref.read(orderApiProvider).orderDetail(widget.orderId); + if (!mounted) return; + if (!resp.isSuccess || resp.data == null) { + setState(() => _state = MultiState.error); + return; + } + setState(() { + _order = resp.data; + // 默认添加一条基础服务费 + if (_items.isEmpty) { + _items.add(AmountItem( + id: 0, + title: '基础服务费', + amount: resp.data!.amount, + type: 'income', + createdAt: DateTime.now(), + orderNo: resp.data!.orderNo, + )); + } + _state = MultiState.success; + }); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + double get _total => _items.fold(0, (s, e) => s + e.amount); + + Future _addItem() async { + final added = await showDialog( + context: context, + builder: (_) => const _AddAmountDialog(), + ); + if (added != null) setState(() => _items.add(added)); + } + + Future _submit() async { + if (_items.isEmpty) { + Toast.error('请至少添加一项费用明细'); + return; + } + setState(() => _submitting = true); + EasyLoading.show(status: '提交中…'); + try { + final r = await ref.read(orderServiceProvider).complete( + amountItems: _items, + remark: _remarkCtrl.text.trim().isEmpty + ? null + : _remarkCtrl.text.trim(), + ); + EasyLoading.dismiss(); + if (r is CompleteOrderInfo) { + Toast.success('订单已完成'); + if (mounted) { + // 通知服务端完成(已在 OrderService 中调用) + context.go(Routes.orderRecord); + } + } + } catch (e) { + EasyLoading.dismiss(); + Toast.error('提交失败:$e'); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('完成订单'), + actions: [ + if (_order != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: Center( + child: Text( + '订单 ${_order!.orderNo}', + style: const TextStyle(fontSize: 12), + ), + ), + ), + ], + ), + body: MultiStateView( + state: _state, + onRetry: _load, + child: _order == null + ? const SizedBox.shrink() + : ListView( + padding: const EdgeInsets.all(12), + children: [ + _summary(), + const SizedBox(height: 12), + _itemList(), + const SizedBox(height: 8), + OutlinedButton.icon( + onPressed: _addItem, + icon: const Icon(Icons.add), + label: const Text('添加费用项'), + ), + const SizedBox(height: 12), + TextField( + controller: _remarkCtrl, + maxLines: 3, + decoration: const InputDecoration( + labelText: '备注', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + const Text('合计:', + style: TextStyle(fontSize: 16)), + Text( + '¥${_total.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.red, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + ElevatedButton( + onPressed: _submitting ? null : _submit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + minimumSize: const Size.fromHeight(48), + ), + child: Text(_submitting ? '提交中…' : '提交完成'), + ), + ], + ), + ), + ); + } + + Widget _summary() { + final o = _order!; + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(o.serviceType, + style: + const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + if (o.address != null) + Text('地点:${o.address}', + style: TextStyle(color: Colors.grey.shade700, fontSize: 13)), + if (o.customerName != null) + Text('客户:${o.customerName!}', + style: TextStyle(color: Colors.grey.shade700, fontSize: 13)), + ], + ), + ), + ); + } + + Widget _itemList() { + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: Column( + children: [ + for (int i = 0; i < _items.length; i++) + ListTile( + title: Text(_items[i].title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('¥${_items[i].amount.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + )), + IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.red), + onPressed: () => + setState(() => _items.removeAt(i)), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// 新增费用项弹窗 +class _AddAmountDialog extends StatefulWidget { + const _AddAmountDialog(); + @override + State<_AddAmountDialog> createState() => _AddAmountDialogState(); +} + +class _AddAmountDialogState extends State<_AddAmountDialog> { + final _title = TextEditingController(); + final _amount = TextEditingController(); + + @override + void dispose() { + _title.dispose(); + _amount.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('添加费用项'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _title, + decoration: const InputDecoration( + labelText: '费用名称', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 8), + TextField( + controller: _amount, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + labelText: '金额', + prefixText: '¥ ', + border: OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () { + final t = _title.text.trim(); + final a = double.tryParse(_amount.text.trim()) ?? 0; + if (t.isEmpty || a <= 0) { + Toast.error('请填写完整'); + return; + } + Navigator.pop( + context, + AmountItem( + id: 0, + title: t, + amount: a, + type: 'income', + createdAt: DateTime.now(), + ), + ); + }, + child: const Text('添加'), + ), + ], + ); + } +} diff --git a/lib/pages/order/conduct_page.dart b/lib/pages/order/conduct_page.dart new file mode 100644 index 0000000..b483585 --- /dev/null +++ b/lib/pages/order/conduct_page.dart @@ -0,0 +1,454 @@ +import 'dart:async'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:autosos_flutter/widget/watermark.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// 救援中页(对应 Android ConductActivity) +/// 步骤:开始 → 到达现场 → 拍照 → 签字 → 收款 → 完成 +class ConductPage extends ConsumerStatefulWidget { + final int orderId; + const ConductPage({super.key, required this.orderId}); + + @override + ConsumerState createState() => _ConductPageState(); +} + +class _ConductPageState extends ConsumerState { + MultiState _state = MultiState.loading; + Order? _order; + Timer? _ticker; + Duration _elapsed = Duration.zero; + + @override + void initState() { + super.initState(); + _load(); + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (!mounted || _order == null) return; + setState(() { + final start = _order!.acceptedAt ?? _order!.createdAt; + _elapsed = DateTime.now().difference(start); + }); + }); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final resp = await ref.read(orderApiProvider).orderDetail(widget.orderId); + if (!mounted) return; + if (!resp.isSuccess || resp.data == null) { + setState(() => _state = MultiState.error); + Toast.error(resp.message ?? '订单不存在'); + return; + } + final o = resp.data!; + setState(() { + _order = o; + _state = MultiState.success; + }); + ref.read(orderServiceProvider).setCurrent(o); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + Future _arrive() async { + EasyLoading.show(status: '上报中…'); + try { + await ref.read(orderServiceProvider).arrive(); + EasyLoading.dismiss(); + Toast.success('已标记到达现场'); + _load(); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('上报失败:$e'); + } + } + + Future _cancel() async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('取消订单'), + content: const Text('确定要取消此订单吗?取消后无法恢复。'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('不取消'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + onPressed: () => Navigator.pop(ctx, true), + child: const Text('确认取消'), + ), + ], + ), + ); + if (ok != true) return; + try { + await ref.read(orderServiceProvider).cancel(); + Toast.success('已取消'); + if (mounted) context.go(Routes.home); + } catch (e) { + Toast.error('取消失败:$e'); + } + } + + Future _call(String? phone) async { + if (phone == null || phone.isEmpty) { + Toast.error('客户未留电话'); + return; + } + final uri = Uri.parse('tel:$phone'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + Toast.error('无法拨打电话'); + } + } + + Future _navigate() async { + final o = _order; + if (o?.lat == null || o?.lng == null) { + Toast.error('暂无位置信息'); + return; + } + // 高德地图 URL Scheme + final uri = Uri.parse( + 'androidamap://navi?sourceApplication=jjsos&lat=${o!.lat}&lon=${o.lng}&dev=0&style=2', + ); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + // 退回到 web 版 + final web = Uri.parse( + 'https://uri.amap.com/navigation?to=${o.lng},${o.lat},${o.address ?? "救援地点"}&mode=car', + ); + await launchUrl(web, mode: LaunchMode.externalApplication); + } + } + + String _fmt(Duration d) { + String two(int n) => n.toString().padLeft(2, '0'); + return '${two(d.inHours)}:${two(d.inMinutes.remainder(60))}:${two(d.inSeconds.remainder(60))}'; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('救援中'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _load), + ], + ), + body: MultiStateView( + state: _state, + onRetry: _load, + child: _order == null + ? const SizedBox.shrink() + : _body(_order!), + ), + ); + } + + Widget _body(Order o) { + final isArrived = o.status == OrderStatus.conducting || + o.status == OrderStatus.photographing || + o.status == OrderStatus.signing || + o.status == OrderStatus.collecting; + return Stack( + children: [ + ListView( + padding: const EdgeInsets.all(12), + children: [ + // 倒计时卡片 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.red.shade400, Colors.red.shade700], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.timer, color: Colors.white), + const SizedBox(width: 8), + Text( + '已耗时 ${_fmt(_elapsed)}', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Text('订单号:${o.orderNo}', + style: + const TextStyle(color: Colors.white, fontSize: 12)), + ], + ), + ), + const SizedBox(height: 12), + // 客户信息 + _section( + title: '客户信息', + children: [ + _kv('联系人', o.customerName ?? '—'), + _kv('电话', o.customerPhone ?? '—'), + if (o.carNo != null) _kv('车牌', o.carNo!), + if (o.carType != null) _kv('车型', o.carType!), + if (o.description != null) _kv('描述', o.description!), + ], + ), + const SizedBox(height: 12), + // 救援地点 + _section( + title: '救援地点', + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Row( + children: [ + const Icon(Icons.location_on, + color: Colors.red, size: 18), + const SizedBox(width: 4), + Expanded( + child: Text( + o.address ?? '—', + style: const TextStyle(fontSize: 14), + ), + ), + TextButton.icon( + onPressed: _navigate, + icon: const Icon(Icons.directions, size: 16), + label: const Text('导航'), + ), + TextButton.icon( + onPressed: () => _call(o.customerPhone), + icon: const Icon(Icons.phone, size: 16), + label: const Text('呼叫'), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + // 步骤流程 + _section( + title: '服务流程', + children: [ + _Step( + active: true, + done: isArrived, + title: '到达现场', + child: isArrived + ? Text('已到达:${o.arrivedAt ?? "—"}', + style: const TextStyle(color: Colors.grey)) + : ElevatedButton( + onPressed: _arrive, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: const Text('我已到达现场'), + ), + ), + _Step( + active: isArrived, + done: o.photoUrls.isNotEmpty, + title: '拍照取证', + child: ElevatedButton( + onPressed: isArrived + ? () => context + .push('${Routes.orderPhotograph}/${o.id}') + .then((_) => _load()) + : null, + child: Text( + o.photoUrls.isEmpty + ? '开始拍照' + : '已拍 ${o.photoUrls.length} 张,继续', + ), + ), + ), + _Step( + active: o.photoUrls.isNotEmpty, + done: o.signatureUrl != null, + title: '客户签字', + child: ElevatedButton( + onPressed: o.photoUrls.isNotEmpty + ? () => context + .push('${Routes.orderAutograph}/${o.id}') + .then((_) => _load()) + : null, + child: const Text('让客户签字'), + ), + ), + _Step( + active: o.signatureUrl != null, + done: o.payMethod != null, + title: '客户付款', + child: ElevatedButton( + onPressed: o.signatureUrl != null + ? () => context + .push('${Routes.orderCollection}/${o.id}') + .then((_) => _load()) + : null, + child: const Text('发起收款'), + ), + ), + _Step( + active: o.payMethod != null, + done: o.status == OrderStatus.completed, + title: '完成订单', + child: ElevatedButton( + onPressed: o.payMethod != null + ? () => context + .push('${Routes.orderComplete}/${o.id}') + .then((_) => _load()) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + ), + child: const Text('提交完成'), + ), + ), + ], + ), + const SizedBox(height: 12), + // 取消订单 + OutlinedButton.icon( + onPressed: _cancel, + icon: const Icon(Icons.cancel_outlined, color: Colors.red), + label: const Text( + '取消订单', + style: TextStyle(color: Colors.red), + ), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + minimumSize: const Size.fromHeight(48), + ), + ), + const SizedBox(height: 80), + ], + ), + // 全屏水印 + const IgnorePointer( + child: Watermark( + text: '啾啾救援 © 内部资料', + ), + ), + ], + ); + } + + Widget _section({required String title, required List children}) { + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: Text( + title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + ), + ...children, + const SizedBox(height: 8), + ], + ), + ); + } + + Widget _kv(String k, String v) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 60, + child: Text(k, + style: TextStyle(color: Colors.grey.shade600, fontSize: 13)), + ), + Expanded( + child: Text(v, style: const TextStyle(fontSize: 14)), + ), + ], + ), + ); +} + +class _Step extends StatelessWidget { + final bool active; + final bool done; + final String title; + final Widget child; + const _Step({ + required this.active, + required this.done, + required this.title, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), + child: Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: done + ? Colors.green + : (active + ? Theme.of(context).primaryColor + : Colors.grey.shade300), + ), + child: Icon( + done ? Icons.check : Icons.circle, + size: 14, + color: Colors.white, + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 70, + child: Text( + title, + style: TextStyle( + fontSize: 14, + color: active ? Colors.black : Colors.grey, + ), + ), + ), + Expanded(child: child), + ], + ), + ); + } +} diff --git a/lib/pages/order/photograph_page.dart b/lib/pages/order/photograph_page.dart new file mode 100644 index 0000000..1249563 --- /dev/null +++ b/lib/pages/order/photograph_page.dart @@ -0,0 +1,226 @@ +import 'dart:io'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/photo_grid_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 拍照取证页(对应 Android CheckPhotoActivity) +/// 自定义相机 + 照片队列 + 备注 + 上传 +class PhotographPage extends ConsumerStatefulWidget { + final int orderId; + const PhotographPage({super.key, required this.orderId}); + + @override + ConsumerState createState() => _PhotographPageState(); +} + +class _PhotographPageState extends ConsumerState { + final List _localPaths = []; + final TextEditingController _remarkCtrl = TextEditingController(); + bool _showCamera = false; + bool _uploading = false; + + @override + void dispose() { + _remarkCtrl.dispose(); + super.dispose(); + } + + void _onCaptured(String path) { + setState(() { + _localPaths.add(path); + // 拍到一张后自动收起相机,让用户确认后再拍 + _showCamera = false; + }); + } + + Future _pickFromAlbum() async { + final files = await AlbumPicker.pick(); + if (files.isEmpty) return; + setState(() => _localPaths.addAll(files)); + } + + Future _submit() async { + if (_localPaths.isEmpty) { + Toast.error('请先拍照'); + return; + } + setState(() => _uploading = true); + EasyLoading.show(status: '上传中…'); + try { + // 1) 走七牛 + final urls = await ref + .read(uploadServiceProvider) + .uploadBatch(_localPaths); + // 2) 通知后端 + final resp = await ref + .read(orderApiProvider) + .uploadPhotos(widget.orderId, urls); + EasyLoading.dismiss(); + if (!resp.isSuccess) { + Toast.error(resp.message ?? '保存失败'); + return; + } + Toast.success('已上传 ${urls.length} 张照片'); + if (mounted) context.pop(true); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('上传失败:$e'); + } finally { + if (mounted) setState(() => _uploading = false); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + title: const Text('拍照取证'), + iconTheme: const IconThemeData(color: Colors.white), + ), + body: SafeArea( + child: Column( + children: [ + // 备注 + Container( + color: Colors.white, + padding: const EdgeInsets.all(12), + child: TextField( + controller: _remarkCtrl, + maxLines: 2, + decoration: const InputDecoration( + hintText: '可填写现场情况描述(选填)', + border: OutlineInputBorder(), + ), + ), + ), + Expanded( + child: _showCamera + ? CameraView(onCaptured: _onCaptured) + : _photoList(), + ), + // 底部按钮区 + Container( + color: Colors.white, + padding: const EdgeInsets.all(8), + child: Column( + children: [ + PhotoThumbStrip( + paths: _localPaths, + onAdd: () => setState(() => _showCamera = true), + onRemove: (i) => + setState(() => _localPaths.removeAt(i)), + ), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _showCamera + ? null + : () => + setState(() => _showCamera = true), + icon: const Icon(Icons.camera_alt), + label: const Text('拍照'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: _showCamera + ? null + : _pickFromAlbum, + icon: const Icon(Icons.photo_library), + label: const Text('相册'), + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 2, + child: ElevatedButton.icon( + onPressed: _uploading ? null : _submit, + icon: const Icon(Icons.cloud_upload), + label: Text( + _uploading + ? '上传中…' + : '完成(${_localPaths.length})', + ), + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).primaryColor, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _photoList() { + if (_localPaths.isEmpty) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.photo_camera, + size: 80, color: Colors.white24), + SizedBox(height: 8), + Text('点击下方"拍照"开始取证', + style: TextStyle(color: Colors.white54)), + ], + ), + ); + } + return GridView.builder( + padding: const EdgeInsets.all(8), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: _localPaths.length, + itemBuilder: (ctx, i) => Stack( + children: [ + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.file( + File(_localPaths[i]), + fit: BoxFit.cover, + ), + ), + ), + Positioned( + top: 2, + right: 2, + child: GestureDetector( + onTap: () => setState(() => _localPaths.removeAt(i)), + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(2), + child: const Icon( + Icons.close, + size: 14, + color: Colors.white, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/order/record_page.dart b/lib/pages/order/record_page.dart new file mode 100644 index 0000000..7da1826 --- /dev/null +++ b/lib/pages/order/record_page.dart @@ -0,0 +1,249 @@ +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +/// 历史订单记录页(对应 Android RecordOrderActivity) +class OrderRecordPage extends ConsumerStatefulWidget { + const OrderRecordPage({super.key}); + + @override + ConsumerState createState() => _OrderRecordPageState(); +} + +class _OrderRecordPageState extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + String? _status; + int _page = 1; + bool _hasMore = true; + bool _loadingMore = false; + final _scroll = ScrollController(); + + final _tabs = const [ + ('全部', null), + ('已完成', 'completed'), + ('已取消', 'cancelled'), + ]; + + @override + void initState() { + super.initState(); + _load(reset: true); + _scroll.addListener(_onScroll); + } + + @override + void dispose() { + _scroll.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scroll.position.pixels >= + _scroll.position.maxScrollExtent - 200) { + _load(); + } + } + + Future _load({bool reset = false}) async { + if (reset) { + _page = 1; + _hasMore = true; + _items = []; + setState(() => _state = MultiState.loading); + } + if (!_hasMore || _loadingMore) return; + _loadingMore = true; + try { + final resp = await ref.read(orderApiProvider).historyOrders( + page: _page, + size: 20, + status: _status, + ); + if (!mounted) return; + if (!resp.isSuccess) { + if (reset) setState(() => _state = MultiState.error); + Toast.error(resp.message ?? '加载失败'); + return; + } + final list = resp.data?.list ?? []; + _items.addAll(list); + _hasMore = (resp.data?.hasMore ?? false) && list.length >= 20; + _page++; + setState(() { + _state = _items.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted && reset) setState(() => _state = MultiState.error); + } finally { + _loadingMore = false; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('历史订单'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(44), + child: Container( + color: Colors.white, + child: Row( + children: _tabs + .map( + (t) => Expanded( + child: InkWell( + onTap: () { + if (_status == t.$2) return; + setState(() => _status = t.$2); + _load(reset: true); + }, + child: Container( + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: _status == t.$2 + ? Theme.of(context).primaryColor + : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + t.$1, + style: TextStyle( + color: _status == t.$2 + ? Theme.of(context).primaryColor + : Colors.black87, + fontWeight: _status == t.$2 + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ), + ), + ) + .toList(), + ), + ), + ), + ), + body: RefreshIndicator( + onRefresh: () => _load(reset: true), + child: MultiStateView( + state: _state, + emptyText: '暂无历史订单', + onRetry: () => _load(reset: true), + child: ListView.separated( + controller: _scroll, + padding: const EdgeInsets.all(12), + itemCount: _items.length + (_hasMore ? 1 : 0), + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) { + if (i >= _items.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator(strokeWidth: 2)), + ); + } + return _RecordCard( + order: _items[i], + onTap: () => context.push( + '${Routes.orderComplete}/${_items[i].id}', + ), + ); + }, + ), + ), + ), + ); + } +} + +class _RecordCard extends StatelessWidget { + final Order order; + final VoidCallback onTap; + const _RecordCard({required this.order, required this.onTap}); + + @override + Widget build(BuildContext context) { + final fmt = DateFormat('yyyy-MM-dd HH:mm'); + final isCompleted = order.status == OrderStatus.completed; + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: isCompleted + ? Colors.green.shade50 + : Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + order.status.label, + style: TextStyle( + color: isCompleted + ? Colors.green.shade700 + : Colors.grey.shade600, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + Text(order.serviceType, + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w600)), + const Spacer(), + Text( + '¥${order.amount.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.red, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Text('订单号:${order.orderNo}', + style: const TextStyle(fontSize: 13)), + if (order.address != null && order.address!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text('地点:${order.address}', + style: TextStyle( + fontSize: 13, color: Colors.grey.shade700)), + ], + const SizedBox(height: 4), + Text('时间:${fmt.format(order.createdAt)}', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500)), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/order/waiting_page.dart b/lib/pages/order/waiting_page.dart new file mode 100644 index 0000000..cf571f3 --- /dev/null +++ b/lib/pages/order/waiting_page.dart @@ -0,0 +1,225 @@ +import 'dart:async'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/toast.dart'; +import 'package:autosos_flutter/widget/multi_state_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +/// 等待接单页(对应 Android WaitOrderActivity) +/// 列出当前可抢订单,倒计时 0 后自动从列表移除 +class WaitingPage extends ConsumerStatefulWidget { + const WaitingPage({super.key}); + + @override + ConsumerState createState() => _WaitingPageState(); +} + +class _WaitingPageState extends ConsumerState { + MultiState _state = MultiState.loading; + List _items = []; + Timer? _ticker; + + @override + void initState() { + super.initState(); + _load(); + // 每秒刷新倒计时 + _ticker = Timer.periodic(const Duration(seconds: 1), (_) { + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + _ticker?.cancel(); + super.dispose(); + } + + Future _load() async { + setState(() => _state = MultiState.loading); + try { + final resp = await ref.read(orderApiProvider).waitingList(); + if (!mounted) return; + if (!resp.isSuccess) { + setState(() => _state = MultiState.error); + Toast.error(resp.message ?? '加载失败'); + return; + } + setState(() { + _items = resp.data ?? []; + _state = _items.isEmpty ? MultiState.empty : MultiState.success; + }); + } catch (e) { + if (mounted) setState(() => _state = MultiState.error); + } + } + + Future _grab(WaitingOrder item) async { + EasyLoading.show(status: '抢单中…'); + try { + final order = await ref.read(orderServiceProvider).grab(item.orderId); + EasyLoading.dismiss(); + Toast.success('接单成功'); + if (!mounted) return; + // 跳转到救援中页 + context.pushReplacement('${Routes.orderConduct}/${order.id}'); + } catch (e) { + EasyLoading.dismiss(); + Toast.error('抢单失败:$e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('等待接单'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), onPressed: _load), + ], + ), + body: RefreshIndicator( + onRefresh: _load, + child: MultiStateView( + state: _state, + emptyText: '当前没有可接订单', + onRetry: _load, + child: ListView.separated( + padding: const EdgeInsets.all(12), + itemCount: _items.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) { + final o = _items[i]; + return _WaitingCard( + item: o, + onGrab: () => _grab(o), + ); + }, + ), + ), + ), + ); + } +} + +class _WaitingCard extends StatelessWidget { + final WaitingOrder item; + final VoidCallback onGrab; + const _WaitingCard({required this.item, required this.onGrab}); + + String _formatCountdown(int s) { + if (s <= 0) return '已超时'; + final m = (s ~/ 60).toString().padLeft(2, '0'); + final sec = (s % 60).toString().padLeft(2, '0'); + return '$m:$sec'; + } + + @override + Widget build(BuildContext context) { + final fmt = DateFormat('MM-dd HH:mm'); + final expired = item.countdown <= 0; + return Material( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + item.serviceType, + style: TextStyle( + color: Colors.red.shade700, + fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + if (item.distance != null) + Text( + '距离 ${(item.distance! / 1000).toStringAsFixed(1)} km', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: expired ? Colors.grey : Colors.orange, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + _formatCountdown(item.countdown), + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text('订单号:${item.orderNo}', + style: const TextStyle(fontSize: 13)), + if (item.address != null && item.address!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text('地点:${item.address}', + style: TextStyle( + fontSize: 13, color: Colors.grey.shade700)), + ], + const SizedBox(height: 4), + Text('时间:${fmt.format(item.createdAt)}', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500)), + const SizedBox(height: 8), + Row( + children: [ + Text( + '¥${item.amount.toStringAsFixed(2)}', + style: const TextStyle( + color: Colors.red, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + ElevatedButton.icon( + onPressed: expired ? null : onGrab, + icon: const Icon(Icons.flash_on, size: 18), + label: const Text('立即抢单'), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/splash/agreement_dialog.dart b/lib/pages/splash/agreement_dialog.dart new file mode 100644 index 0000000..621c70e --- /dev/null +++ b/lib/pages/splash/agreement_dialog.dart @@ -0,0 +1,40 @@ +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// 隐私协议 + 服务协议弹窗(对应 Android ExplainXpopup) +Future showAgreementDialog(BuildContext context) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('用户协议与隐私政策'), + content: const SingleChildScrollView( + child: Text( + '在你使用之前,请仔细阅读《服务协议》和《隐私政策》的全部条款。\n\n' + '我们将依法保护你的个人信息安全,包括位置信息、设备信息等。\n\n' + '点击"同意并继续"即代表你接受上述协议。', + style: TextStyle(fontSize: 14, height: 1.6), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('不同意并退出'), + ), + TextButton( + onPressed: () => ctx.push('${Routes.webview}?url=https://t.jjsos.cn/index_xy.html&title=服务协议'), + child: const Text('服务协议'), + ), + TextButton( + onPressed: () => ctx.push('${Routes.webview}?url=https://t.jjsos.cn/index_ys.html&title=隐私政策'), + child: const Text('隐私政策'), + ), + ElevatedButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('同意并继续'), + ), + ], + ), + ); +} \ No newline at end of file diff --git a/lib/pages/splash/splash_page.dart b/lib/pages/splash/splash_page.dart new file mode 100644 index 0000000..cd6f0c2 --- /dev/null +++ b/lib/pages/splash/splash_page.dart @@ -0,0 +1,128 @@ +import 'package:autosos_flutter/config/theme_colors.dart'; +import 'package:autosos_flutter/pages/splash/agreement_dialog.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:permission_handler/permission_handler.dart'; + +/// 启动页(对应 Android SplashActivity) +class SplashPage extends ConsumerStatefulWidget { + const SplashPage({super.key}); + + @override + ConsumerState createState() => _SplashPageState(); +} + +class _SplashPageState extends ConsumerState { + final String _pushType = '0'; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrap()); + } + + Future _bootstrap() async { + // 读取推送 type 参数 + // (真实使用中可以从 notification 启动 Intent 拿到) + + final agreed = SPUtil().get('isAgreement') ?? false; + if (!agreed) { + final ok = await showAgreementDialog(context); + if (ok != true) return; + SPUtil().setBool('isAgreement', true); + } + + // 请求定位权限 + final locGranted = await _requestLocation(); + if (!locGranted) { + // 提示去设置 + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请到设置中开启位置权限')), + ); + } + return; + } + + _navigateNext(); + } + + Future _requestLocation() async { + final status = await Permission.locationWhenInUse.request(); + if (!status.isGranted) return false; + // 后台定位(Android 10+ 需要) + if (await Permission.locationAlways.isDenied) { + await Permission.locationAlways.request(); + } + await LocationService.instance.init(); + LocationService.instance.start(); + return true; + } + + void _navigateNext() { + final logged = ref.read(isLoggedInProvider); + final welcomed = SPUtil().get('isWelcome') ?? false; + + if (!logged) { + context.go(Routes.login); + } else if (!welcomed) { + context.go(Routes.welcome); + } else { + _routeByPushType(); + } + } + + void _routeByPushType() { + final logged = ref.read(isLoggedInProvider); + if (!logged) { + context.go(Routes.login); + return; + } + switch (_pushType) { + case '1': + context.go(Routes.orderWaiting); + break; + case '2': + context.go(Routes.messenger); + break; + default: + context.go(Routes.home); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: ThemeColors.primary, + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'images/1.5x/ic_launcher.png', + width: 96, + height: 96, + ), + const SizedBox(height: 16), + const Text( + '啾啾救援', + style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text( + '宁波易到互联科技有限公司', + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + const SizedBox(height: 32), + const CircularProgressIndicator(color: Colors.white, strokeWidth: 2), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/webview/agent_web_page.dart b/lib/pages/webview/agent_web_page.dart new file mode 100644 index 0000000..4f07fcc --- /dev/null +++ b/lib/pages/webview/agent_web_page.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:go_router/go_router.dart'; + +/// WebView 容器(替代 Android AgentWeb) +/// 支持:进度条 / 标题自动同步 / 返回键关闭 / 拦截外链 / JS 桥 +class AgentWebPage extends StatefulWidget { + final String url; + final String title; + const AgentWebPage({ + super.key, + required this.url, + required this.title, + }); + + @override + State createState() => _AgentWebPageState(); +} + +class _AgentWebPageState extends State { + InAppWebViewController? _controller; + double _progress = 0; + String _title = ''; + bool _canGoBack = false; + + @override + void initState() { + super.initState(); + _title = widget.title; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_title.isEmpty ? '加载中…' : _title), + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => context.pop(), + ), + actions: [ + if (_canGoBack) + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => _controller?.goBack(), + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () => _controller?.reload(), + ), + PopupMenuButton( + onSelected: (v) async { + switch (v) { + case 'copy': + // ignore: deprecated_member_use + // await Clipboard.setData(ClipboardData(text: widget.url)); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('链接已复制')), + ); + } + break; + case 'browser': + // ignore: deprecated_member_use + // await launchUrl(Uri.parse(widget.url)); + break; + } + }, + itemBuilder: (_) => const [ + PopupMenuItem(value: 'copy', child: Text('复制链接')), + PopupMenuItem(value: 'browser', child: Text('浏览器打开')), + ], + ), + ], + bottom: _progress < 1 + ? PreferredSize( + preferredSize: const Size.fromHeight(2), + child: LinearProgressIndicator( + value: _progress, + backgroundColor: Colors.white24, + valueColor: const AlwaysStoppedAnimation(Colors.white), + ), + ) + : null, + ), + body: InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + javaScriptCanOpenWindowsAutomatically: true, + useShouldOverrideUrlLoading: true, + mediaPlaybackRequiresUserGesture: false, + allowFileAccess: true, + domStorageEnabled: true, + databaseEnabled: true, + cacheEnabled: true, + supportZoom: true, + transparentBackground: false, + ), + onWebViewCreated: (c) => _controller = c, + onProgressChanged: (_, p) { + if (!mounted) return; + setState(() => _progress = p / 100); + }, + onTitleChanged: (_, t) { + if (!mounted) return; + setState(() => _title = t ?? widget.title); + }, + onUpdateVisitedHistory: (_, url, __) { + _controller?.canGoBack().then((v) { + if (mounted) setState(() => _canGoBack = v); + }); + }, + shouldOverrideUrlLoading: (_, nav) async { + final uri = nav.request.url; + if (uri == null) return NavigationActionPolicy.ALLOW; + // 拦截 tel:/mailto:/weixin:/alipays: + final s = uri.toString(); + if (s.startsWith('tel:') || + s.startsWith('mailto:') || + s.startsWith('weixin://') || + s.startsWith('alipays://')) { + return NavigationActionPolicy.CANCEL; + } + return NavigationActionPolicy.ALLOW; + }, + ), + ); + } +} diff --git a/lib/pages/welcome/welcome_page.dart b/lib/pages/welcome/welcome_page.dart new file mode 100644 index 0000000..39bf339 --- /dev/null +++ b/lib/pages/welcome/welcome_page.dart @@ -0,0 +1,129 @@ +import 'package:autosos_flutter/router/app_router.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// 新手引导页(对应 Android WelcomeActivity + ViewPager) +class WelcomePage extends StatefulWidget { + const WelcomePage({super.key}); + + @override + State createState() => _WelcomePageState(); +} + +class _WelcomePageState extends State { + final _controller = PageController(); + int _index = 0; + + final _pages = const [ + _WelcomeItem( + img: 'images/4.0x/welcome_1.png', + title: '快速接单', + subtitle: '实时推送附近订单,第一时间响应', + ), + _WelcomeItem( + img: 'images/4.0x/welcome_2.png', + title: '拍照取证', + subtitle: '现场拍照、客户签字,全流程电子化', + ), + _WelcomeItem( + img: 'images/4.0x/welcome_3.png', + title: '收入透明', + subtitle: '订单金额、提现记录一目了然', + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + PageView.builder( + controller: _controller, + onPageChanged: (i) => setState(() => _index = i), + itemCount: _pages.length, + itemBuilder: (_, i) => _pages[i], + ), + Positioned( + bottom: 40, + left: 0, + right: 0, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _pages.length, + (i) => AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.symmetric(horizontal: 4), + width: _index == i ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: _index == i + ? Theme.of(context).primaryColor + : Colors.grey.shade400, + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _index == _pages.length - 1 + ? _finish + : () => _controller.nextPage( + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + ), + child: Text(_index == _pages.length - 1 ? '立即体验' : '下一步'), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Future _finish() async { + SPUtil().setBool('isWelcome', true); + if (!mounted) return; + context.go(Routes.home); + } +} + +class _WelcomeItem extends StatelessWidget { + final String img; + final String title; + final String subtitle; + + const _WelcomeItem({required this.img, required this.title, required this.subtitle}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(img, fit: BoxFit.contain, height: 280), + const SizedBox(height: 32), + Text(title, + style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Text(subtitle, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey.shade600)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/provider/providers.dart b/lib/provider/providers.dart new file mode 100644 index 0000000..f584077 --- /dev/null +++ b/lib/provider/providers.dart @@ -0,0 +1,46 @@ +import 'package:autosos_flutter/api/order_api.dart'; +import 'package:autosos_flutter/api/user_api.dart'; +import 'package:autosos_flutter/api/upload_api.dart'; +import 'package:autosos_flutter/model/user.dart'; +import 'package:autosos_flutter/service/audio_service.dart'; +import 'package:autosos_flutter/service/background_service.dart'; +import 'package:autosos_flutter/service/heartbeat_service.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:autosos_flutter/service/order_service.dart'; +import 'package:autosos_flutter/service/push_service.dart'; +import 'package:autosos_flutter/service/update_service.dart'; +import 'package:autosos_flutter/service/upload_service.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// 全局 Provider 入口(所有页面通过 ref.watch/read 拿到服务) + +// ===== 单例服务 ===== +final spUtilProvider = Provider((_) => SPUtil()); + +final locationServiceProvider = Provider((_) => LocationService.instance); +final pushServiceProvider = Provider((_) => PushService.instance); +final heartbeatServiceProvider = Provider((_) => HeartbeatService.instance); +final audioServiceProvider = Provider((_) => AudioService.instance); +final backgroundServiceProvider = Provider((_) => BackgroundService.instance); +final orderServiceProvider = Provider((_) => OrderService.instance); +final uploadServiceProvider = Provider((_) => UploadService.instance); +final updateServiceProvider = Provider((_) => UpdateService.instance); + +// ===== API ===== +final orderApiProvider = Provider((_) => OrderApi.instance); +final userApiProvider = Provider((_) => UserApi.instance); +final uploadApiProvider = Provider((_) => UploadApi.instance); + +// ===== 登录态 ===== +final accessTokenProvider = StateProvider((ref) { + return SPUtil().get('accessToken') as String?; +}); + +final isLoggedInProvider = Provider((ref) { + final t = ref.watch(accessTokenProvider); + return t != null && t.isNotEmpty; +}); + +/// 当前登录用户(启动时尝试从缓存恢复,进入首页后异步刷新) +final currentUserProvider = StateProvider((ref) => null); \ No newline at end of file diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart new file mode 100644 index 0000000..2730ba5 --- /dev/null +++ b/lib/router/app_router.dart @@ -0,0 +1,148 @@ +import 'package:autosos_flutter/pages/home/home_page.dart'; +import 'package:autosos_flutter/pages/login/login_page.dart'; +import 'package:autosos_flutter/pages/login/bind_phone_page.dart'; +import 'package:autosos_flutter/pages/login/bind_wx_page.dart'; +import 'package:autosos_flutter/pages/me/me_page.dart'; +import 'package:autosos_flutter/pages/me/authentication_page.dart'; +import 'package:autosos_flutter/pages/me/perfect_page.dart'; +import 'package:autosos_flutter/pages/me/withdrawal_page.dart'; +import 'package:autosos_flutter/pages/me/withdrawal_record_page.dart'; +import 'package:autosos_flutter/pages/me/recharge_page.dart'; +import 'package:autosos_flutter/pages/me/amount_list_page.dart'; +import 'package:autosos_flutter/pages/me/feedback_page.dart'; +import 'package:autosos_flutter/pages/me/messenger_page.dart'; +import 'package:autosos_flutter/pages/me/setting_page.dart'; +import 'package:autosos_flutter/pages/me/optimization_page.dart'; +import 'package:autosos_flutter/pages/order/check_page.dart'; +import 'package:autosos_flutter/pages/order/waiting_page.dart'; +import 'package:autosos_flutter/pages/order/conduct_page.dart'; +import 'package:autosos_flutter/pages/order/photograph_page.dart'; +import 'package:autosos_flutter/pages/order/autograph_page.dart'; +import 'package:autosos_flutter/pages/order/collection_page.dart'; +import 'package:autosos_flutter/pages/order/complete_page.dart'; +import 'package:autosos_flutter/pages/order/record_page.dart'; +import 'package:autosos_flutter/pages/splash/splash_page.dart'; +import 'package:autosos_flutter/pages/welcome/welcome_page.dart'; +import 'package:autosos_flutter/pages/webview/agent_web_page.dart'; +import 'package:autosos_flutter/pages/common/qr_scan_page.dart'; +import 'package:autosos_flutter/provider/providers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +/// 路由名集中定义 +class Routes { + static const splash = '/'; + static const welcome = '/welcome'; + static const login = '/login'; + static const bindPhone = '/bind-phone'; + static const bindWx = '/bind-wx'; + + static const home = '/home'; + static const me = '/me'; + static const perfect = '/me/perfect'; + static const auth = '/me/auth'; + static const withdrawal = '/me/withdrawal'; + static const withdrawalRecord = '/me/withdrawal/record'; + static const recharge = '/me/recharge'; + static const amountList = '/me/amount'; + static const feedback = '/me/feedback'; + static const messenger = '/me/messenger'; + static const setting = '/me/setting'; + static const optimization = '/me/optimization'; + + static const orderCheck = '/order/check'; + static const orderWaiting = '/order/waiting'; + static const orderConduct = '/order/conduct'; + static const orderPhotograph = '/order/photograph'; + static const orderAutograph = '/order/autograph'; + static const orderCollection = '/order/collection'; + static const orderComplete = '/order/complete'; + static const orderRecord = '/order/record'; + + static const webview = '/webview'; + static const qrScan = '/qr-scan'; +} + +final routerProvider = Provider((ref) { + return GoRouter( + initialLocation: Routes.splash, + redirect: (context, state) { + final logged = ref.read(isLoggedInProvider); + final loc = state.matchedLocation; + final isAuthRoute = loc == Routes.login || loc == Routes.bindPhone || loc == Routes.bindWx; + + // 未登录 → 跳登录 + if (!logged && !isAuthRoute && loc != Routes.splash && loc != Routes.welcome) { + return Routes.login; + } + // 已登录 → 不能留在登录页 + if (logged && isAuthRoute) { + return Routes.home; + } + return null; + }, + refreshListenable: _AuthListenable(ref), + routes: [ + GoRoute(path: Routes.splash, builder: (_, __) => const SplashPage()), + GoRoute(path: Routes.welcome, builder: (_, __) => const WelcomePage()), + GoRoute(path: Routes.login, builder: (_, __) => const LoginPage()), + GoRoute(path: Routes.bindPhone, builder: (_, __) => const BindPhonePage()), + GoRoute(path: Routes.bindWx, builder: (_, __) => const BindWxPage()), + + GoRoute(path: Routes.home, builder: (_, __) => const HomePage()), + GoRoute(path: Routes.me, builder: (_, __) => const MePage()), + GoRoute(path: Routes.perfect, builder: (_, __) => const PerfectPage()), + GoRoute(path: Routes.auth, builder: (_, __) => const AuthenticationPage()), + GoRoute(path: Routes.withdrawal, builder: (_, __) => const WithdrawalPage()), + GoRoute(path: Routes.withdrawalRecord, builder: (_, __) => const WithdrawalRecordPage()), + GoRoute(path: Routes.recharge, builder: (_, __) => const RechargePage()), + GoRoute(path: Routes.amountList, builder: (_, __) => const AmountListPage()), + GoRoute(path: Routes.feedback, builder: (_, __) => const FeedbackPage()), + GoRoute(path: Routes.messenger, builder: (_, __) => const MessengerPage()), + GoRoute(path: Routes.setting, builder: (_, __) => const SettingPage()), + GoRoute(path: Routes.optimization, builder: (_, __) => const OptimizationPage()), + + GoRoute(path: Routes.orderCheck, builder: (_, __) => const CheckPage()), + GoRoute(path: Routes.orderWaiting, builder: (_, __) => const WaitingPage()), + GoRoute( + path: '${Routes.orderConduct}/:id', + builder: (_, s) => ConductPage(orderId: int.parse(s.pathParameters['id']!)), + ), + GoRoute( + path: '${Routes.orderPhotograph}/:id', + builder: (_, s) => PhotographPage(orderId: int.parse(s.pathParameters['id']!)), + ), + GoRoute( + path: '${Routes.orderAutograph}/:id', + builder: (_, s) => AutographPage(orderId: int.parse(s.pathParameters['id']!)), + ), + GoRoute( + path: '${Routes.orderCollection}/:id', + builder: (_, s) => CollectionPage(orderId: int.parse(s.pathParameters['id']!)), + ), + GoRoute( + path: '${Routes.orderComplete}/:id', + builder: (_, s) => CompletePage(orderId: int.parse(s.pathParameters['id']!)), + ), + GoRoute(path: Routes.orderRecord, builder: (_, __) => const OrderRecordPage()), + + GoRoute( + path: Routes.webview, + builder: (_, s) => AgentWebPage( + url: s.uri.queryParameters['url'] ?? '', + title: s.uri.queryParameters['title'] ?? '', + ), + ), + GoRoute(path: Routes.qrScan, builder: (_, __) => const QrScanPage()), + ], + ); +}); + +/// 把 token 变更同步给 GoRouter 触发 redirect +class _AuthListenable extends ChangeNotifier { + _AuthListenable(this.ref) { + ref.listen(accessTokenProvider, (_, __) => notifyListeners()); + } + final Ref ref; +} \ No newline at end of file diff --git a/lib/service/audio_service.dart b/lib/service/audio_service.dart new file mode 100644 index 0000000..fd1429d --- /dev/null +++ b/lib/service/audio_service.dart @@ -0,0 +1,49 @@ +import 'package:audioplayers/audioplayers.dart'; +import 'package:flutter/foundation.dart'; + +/// 音频播放服务(对应 Android SoundPoolUtils) +/// +/// 用 AudioPlayers(Flutter 包)替代 Android 的 SoundPool, +/// 提供短音效(订单提示、警告音)的低延迟播放能力。 +class AudioService { + static final AudioService instance = AudioService._(); + AudioService._(); + + final _player = AudioPlayer(); + + /// 是否启用声音提醒(用户设置开关) + bool enableSound = true; + + /// 初始化播放源(使用 AssetSource) + Future _play(String assetPath, {double volume = 1.0}) async { + if (!enableSound) return; + try { + await _player.stop(); + await _player.setReleaseMode(ReleaseMode.stop); + await _player.setVolume(volume); + await _player.play(AssetSource(assetPath)); + } catch (e) { + debugPrint('AudioService play failed: $e'); + } + } + + /// 新订单到来(救援平台内部订单) + Future newInsideOrder() => _play('sounds/new_order_inside.mp3'); + + /// 新订单(啾啾救援订单) + Future newJiuJiuOrder() => _play('sounds/new_order_jiujiu.mp3'); + + /// 订单被关闭 + Future orderClosed() => _play('sounds/order_closed.mp3'); + + /// 通用警告 + Future warning() => _play('sounds/warning.mp3'); + + /// 自定义音效 + Future custom(String assetPath, {double volume = 1.0}) => + _play(assetPath, volume: volume); + + void dispose() { + _player.dispose(); + } +} \ No newline at end of file diff --git a/lib/service/background_service.dart b/lib/service/background_service.dart new file mode 100644 index 0000000..8bc7e34 --- /dev/null +++ b/lib/service/background_service.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:autosos_flutter/service/heartbeat_service.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_background_service/flutter_background_service.dart'; + +/// 后台保活服务(对应 Android JjSosLongService) +/// +/// 启动后会在原生层持有一个前台服务(Foreground Service), +/// 即使 App 被系统回收,isolate 仍可继续执行心跳与位置上报。 +class BackgroundService { + static final BackgroundService instance = BackgroundService._(); + BackgroundService._(); + + static const String _notificationChannelId = 'autosos_bg_channel'; + static const int _notificationId = 888; + + final _service = FlutterBackgroundService(); + + /// 是否正在运行 + Future isRunning() => _service.isRunning(); + + /// 初始化(在 main() 里调用一次) + Future init() async { + if (!Platform.isAndroid && !Platform.isIOS) return; + + await _service.configure( + androidConfiguration: AndroidConfiguration( + onStart: _onStart, + isForegroundMode: true, + autoStart: true, + autoStartOnBoot: true, + foregroundServiceNotificationId: _notificationId, + initialNotificationTitle: '啾啾救援', + initialNotificationContent: '正在保持在线...', + notificationChannelId: _notificationChannelId, + foregroundServiceTypes: const [ + AndroidForegroundType.location, + AndroidForegroundType.dataSync, + ], + ), + iosConfiguration: IosConfiguration( + autoStart: true, + onForeground: _onStart, + onBackground: _onIosBackground, + ), + ); + } + + /// 启动后台服务 + Future start() async { + final running = await _service.isRunning(); + if (!running) { + await _service.startService(); + } + } + + /// 停止后台服务 + Future stop() async { + _service.invoke('stopService'); + } + + /// Android 入口(独立 isolate) + @pragma('vm:entry-point') + static void _onStart(ServiceInstance service) async { + if (service is AndroidServiceInstance) { + service.on('setAsForeground').listen((event) { + service.setAsForegroundService(); + }); + service.on('setAsBackground').listen((event) { + service.setAsBackgroundService(); + }); + service.on('stopService').listen((event) async { + await service.stopSelf(); + }); + } + + // 启动位置采集 + try { + LocationService.instance.init(); + LocationService.instance.start(); + } catch (e) { + debugPrint('BG location start failed: $e'); + } + + // 启动心跳 + HeartbeatService.instance.start(); + + // 持续保活:每 30 秒检查一次 + Timer.periodic(const Duration(seconds: 30), (timer) async { + if (!HeartbeatService.instance.running) { + HeartbeatService.instance.start(); + } + }); + + debugPrint('Background service started'); + } + + /// iOS 后台入口(必须尽快返回) + @pragma('vm:entry-point') + static bool _onIosBackground(ServiceInstance service) { + // iOS 不允许常驻后台任务,只能在限时窗口内上报 + return true; + } +} diff --git a/lib/service/heartbeat_service.dart b/lib/service/heartbeat_service.dart new file mode 100644 index 0000000..c1bf94e --- /dev/null +++ b/lib/service/heartbeat_service.dart @@ -0,0 +1,62 @@ +import 'dart:async'; +import 'package:autosos_flutter/api/user_api.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:flutter/foundation.dart'; + +/// 心跳服务(保持在线状态,上报位置) +class HeartbeatService { + static final HeartbeatService instance = HeartbeatService._(); + HeartbeatService._(); + + Timer? _timer; + final Duration _interval = const Duration(seconds: 30); + int _onlineSeconds = 0; + bool _running = false; + + bool get running => _running; + int get onlineSeconds => _onlineSeconds; + + /// 启动心跳 + void start() { + if (_running) return; + _running = true; + _onlineSeconds = 0; + _timer = Timer.periodic(_interval, (_) => _tick()); + debugPrint('Heartbeat started'); + } + + /// 停止心跳 + void stop() { + _timer?.cancel(); + _timer = null; + _running = false; + _onlineSeconds = 0; + debugPrint('Heartbeat stopped'); + } + + Future _tick() async { + _onlineSeconds += _interval.inSeconds; + try { + final loc = LocationService.instance.last; + await UserApi.instance.heartbeat( + lat: (loc?['latitude'] as num?)?.toDouble(), + lng: (loc?['longitude'] as num?)?.toDouble(), + onlineTime: _onlineSeconds, + ); + Constant.heartbeatTime = DateTime.now().millisecondsSinceEpoch; + } catch (e) { + debugPrint('Heartbeat tick failed: $e'); + Constant.weberror = true; + } + } + + /// 重置在线时长(接单/完成后调用) + void resetOnlineTime() { + _onlineSeconds = 0; + } + + void dispose() { + stop(); + } +} \ No newline at end of file diff --git a/lib/service/location_service.dart b/lib/service/location_service.dart new file mode 100644 index 0000000..0d83e52 --- /dev/null +++ b/lib/service/location_service.dart @@ -0,0 +1,93 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:amap_flutter_location/amap_flutter_location.dart'; +import 'package:amap_flutter_location/amap_location_option.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:flutter/foundation.dart'; + +/// 定位服务(封装 AMap Flutter Location 3.0) +class LocationService { + static final LocationService instance = LocationService._(); + LocationService._(); + + final _location = AMapFlutterLocation(); + final _positionController = + StreamController>.broadcast(); + + Stream> get positionStream => _positionController.stream; + + Map? _last; + Map? get last => _last; + + bool _initialized = false; + + /// 初始化并请求权限 + Future init({ + String androidKey = '', + String iosKey = '', + }) async { + try { + if (_initialized) return true; + AMapFlutterLocation.setApiKey(androidKey, iosKey); + + _location.setLocationOption(AMapLocationOption( + locationMode: AMapLocationMode.Hight_Accuracy, + onceLocation: false, + locationInterval: 5000, // 5 秒 + desiredAccuracy: DesiredAccuracy.Best, + distanceFilter: -1, + needAddress: true, + )); + + _location.onLocationChanged().listen((event) { + _last = event; + Constant.lat = (event['latitude'] as num?)?.toDouble() ?? 0; + Constant.lng = (event['longitude'] as num?)?.toDouble() ?? 0; + Constant.address = (event['address'] as String?) ?? ''; + _positionController.add(event); + }); + + _initialized = true; + return true; + } catch (e) { + debugPrint('LocationService init failed: $e'); + return false; + } + } + + /// 开始定位 + void start() { + _location.startLocation(); + } + + /// 停止定位 + void stop() { + _location.stopLocation(); + } + + /// 单次定位(用于初始定位、报点等) + Future?> fetchOnce() async { + start(); + final completer = Completer?>(); + final sub = positionStream.listen((loc) { + if (!completer.isCompleted) completer.complete(loc); + }); + final loc = await completer.future.timeout( + const Duration(seconds: 10), + onTimeout: () => null, + ); + await sub.cancel(); + return loc; + } + + void dispose() { + _location.stopLocation(); + _location.destroy(); + _positionController.close(); + } + + /// 平台判断辅助 + static bool get isAndroid => Platform.isAndroid; + static bool get isIOS => Platform.isIOS; +} diff --git a/lib/service/order_service.dart b/lib/service/order_service.dart new file mode 100644 index 0000000..8d4077d --- /dev/null +++ b/lib/service/order_service.dart @@ -0,0 +1,142 @@ +import 'dart:async'; +import 'package:autosos_flutter/api/order_api.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:autosos_flutter/model/order.dart'; +import 'package:autosos_flutter/model/payment.dart'; +import 'package:autosos_flutter/service/audio_service.dart'; +import 'package:autosos_flutter/service/location_service.dart'; +import 'package:flutter/foundation.dart'; + +/// 订单状态机 + 业务编排服务(对应 Android 订单状态机) +class OrderService { + static final OrderService instance = OrderService._(); + OrderService._(); + + Order? _current; + Order? get currentOrder => _current; + + /// 状态变化回调 + final _stateController = StreamController.broadcast(); + Stream get stateStream => _stateController.stream; + + /// 救援中(conducting)→ 上报当前位置的轨迹点 + Timer? _trackUploadTimer; + + /// 设置当前订单 + void setCurrent(Order? order) { + _current = order; + if (order != null) { + Constant.isService = order.status == OrderStatus.conducting; + _stateController.add(OrderStateEvent.updated(order)); + } + } + + /// 接单 + Future grab(int orderId) async { + final resp = await OrderApi.instance.grabOrder(orderId); + if (!resp.isSuccess) throw Exception(resp.message ?? '接单失败'); + final order = resp.data!; + setCurrent(order); + return order; + } + + /// 到达现场 + Future arrive({double? lat, double? lng}) async { + if (_current == null) throw Exception('当前无进行中订单'); + final loc = LocationService.instance.last; + final r = await OrderApi.instance.arriveScene( + _current!.id, + lat: lat ?? (loc?['latitude'] as num?)?.toDouble(), + lng: lng ?? (loc?['longitude'] as num?)?.toDouble(), + ); + if (!r.isSuccess) throw Exception(r.message ?? '上报到达失败'); + final updated = _current!.copyWith( + status: OrderStatus.conducting, + arrivedAt: DateTime.now(), + ); + setCurrent(updated); + _startTrack(); + } + + /// 完成订单 + Future complete({ + required List amountItems, + String? remark, + }) async { + if (_current == null) throw Exception('当前无订单'); + final r = await OrderApi.instance.completeOrder( + orderId: _current!.id, + amountItems: amountItems, + remark: remark, + ); + if (!r.isSuccess) throw Exception(r.message ?? '完成订单失败'); + _stopTrack(); + setCurrent(_current!.copyWith( + status: OrderStatus.completed, + completedAt: DateTime.now(), + )); + return r.data; + } + + /// 取消订单 + Future cancel() async { + if (_current == null) return; + _stopTrack(); + setCurrent(_current!.copyWith(status: OrderStatus.cancelled)); + } + + /// 启动轨迹上传 + void _startTrack() { + _trackUploadTimer?.cancel(); + _trackUploadTimer = Timer.periodic(const Duration(seconds: 10), (_) async { + if (_current == null) return; + final loc = LocationService.instance.last; + if (loc == null) return; + try { + await OrderApi.instance.uploadTrackPoints(_current!.id, [ + { + 'lat': (loc['latitude'] as num?)?.toDouble() ?? 0, + 'lng': (loc['longitude'] as num?)?.toDouble() ?? 0, + 'time': DateTime.now().toIso8601String(), + 'speed': (loc['speed'] as num?)?.toDouble() ?? 0, + 'bearing': (loc['bearing'] as num?)?.toDouble() ?? 0, + } + ]); + } catch (e) { + debugPrint('Track upload failed: $e'); + } + }); + } + + /// 停止轨迹上传 + void _stopTrack() { + _trackUploadTimer?.cancel(); + _trackUploadTimer = null; + } + + /// 收到新订单推送时触发本地语音提醒 + Future onPushNewOrder(Map payload) async { + final type = (payload['type'] as num?)?.toInt() ?? 0; + if (type == Constant.NEW_ORDER_INSIDE) { + AudioService.instance.newInsideOrder(); + } else if (type == Constant.NEW_ORDER_JIUJIU) { + AudioService.instance.newJiuJiuOrder(); + } + } + + void dispose() { + _stopTrack(); + _stateController.close(); + } +} + +/// 订单状态变化事件 +class OrderStateEvent { + final OrderStateEventType type; + final Order? order; + const OrderStateEvent(this.type, this.order); + + factory OrderStateEvent.updated(Order o) => OrderStateEvent(OrderStateEventType.updated, o); +} + +enum OrderStateEventType { updated, completed, cancelled, paid } \ No newline at end of file diff --git a/lib/service/push_service.dart b/lib/service/push_service.dart new file mode 100644 index 0000000..9750ca6 --- /dev/null +++ b/lib/service/push_service.dart @@ -0,0 +1,194 @@ +import 'dart:async'; + +import 'package:autosos_flutter/config/getui_constant.dart'; +import 'package:autosos_flutter/util/sp_util.dart'; +import 'package:flutter/foundation.dart'; +import 'package:getuiflut/getuiflut.dart'; + +/// 推送服务(对应 Android 个推封装) +/// 使用 getuiflut 0.2.x 实际 API(addEventHandler 需要全部回调) +class PushService { + static final PushService instance = PushService._(); + PushService._(); + + String? _clientId; + String? get clientId => _clientId; + + final _payloadController = StreamController>.broadcast(); + Stream> get payloadStream => _payloadController.stream; + + final _notificationController = StreamController>.broadcast(); + Stream> get notificationStream => _notificationController.stream; + + bool _started = false; + + /// 启动个推 SDK + void init() { + try { + if (_started) return; + + // Android 通过 initGetuiSdk 启动(自动) + // iOS 通过 startSdk 启动 + if (defaultTargetPlatform == TargetPlatform.iOS) { + Getuiflut().startSdk( + appId: GetuiConstant.appId, + appKey: GetuiConstant.appKey, + appSecret: GetuiConstant.appSecret, + ); + } else { + Getuiflut().initGetuiSdk; + } + + // 注册事件回调(addEventHandler 全部为 required) + Getuiflut().addEventHandler( + onReceiveClientId: (String message) async { + _clientId = message; + SPUtil().setString('clientId', message); + debugPrint('Getui ClientId: $message'); + }, + + onRegisterDeviceToken: (String token) async { + debugPrint('Getui device token: $token'); + }, + onNotificationMessageArrived: (Map msg) async { + debugPrint('Getui notification arrived: $msg'); + _notificationController.add(msg); + }, + onNotificationMessageClicked: (Map msg) async { + debugPrint('Getui notification clicked: $msg'); + _notificationController.add({...msg, '_clicked': true}); + }, + onTransmitUserMessageReceive: (Map msg) async { + debugPrint('Getui transmit msg: $msg'); + }, + onReceiveOnlineState: (String state) async { + debugPrint('Getui online state: $state'); + }, + onReceivePayload: (Map message) async { + debugPrint('Getui receive payload: $message'); + _payloadController.add(message); + }, + onReceiveNotificationResponse: (Map message) async { + debugPrint('Getui receive notification response: $message'); + }, + onAppLinkPayload: (String message) async { + debugPrint('Getui app link: $message'); + }, + onPushModeResult: (Map message) async { + debugPrint('Getui push mode: $message'); + }, + onSetTagResult: (Map message) async { + debugPrint('Getui set tag result: $message'); + }, + onAliasResult: (Map message) async { + debugPrint('Getui alias result: $message'); + }, + onQueryTagResult: (Map message) async { + debugPrint('Getui query tag result: $message'); + }, + onWillPresentNotification: (Map message) async { + debugPrint('Getui will present: $message'); + }, + onOpenSettingsForNotification: (Map message) async { + debugPrint('Getui open settings: $message'); + }, + onGrantAuthorization: (String granted) async { + debugPrint('Getui grant auth: $granted'); + }, + onLiveActivityResult: (Map message) async { + debugPrint('Getui live activity: $message'); + }, + onRegisterPushToStartTokenResult: (Map message) async { + debugPrint('Getui push to start: $message'); + }, + ); + + _started = true; + } catch (e) { + debugPrint('PushService init failed: $e'); + } + } + + /// 关闭推送通道(用户退出登录后) + void turnOff() { + try { + Getuiflut().turnOffPush(); + } catch (e) { + debugPrint('turnOff push failed: $e'); + } + } + + /// 开启推送 + void turnOn() { + try { + Getuiflut().turnOnPush(); + } catch (e) { + debugPrint('turnOn push failed: $e'); + } + } + + /// 获取 clientId(异步) + Future fetchClientId() async { + try { + final id = await Getuiflut().getClientId; + _clientId = id; + if (id.isNotEmpty) { + SPUtil().setString('clientId', id); + } + return id; + } catch (e) { + debugPrint('fetchClientId failed: $e'); + return null; + } + } + + /// 绑定用户别名(手机号 + sn 时间戳) + void bindAlias(String alias, String sn) { + try { + Getuiflut().bindAlias(alias, sn); + } catch (e) { + debugPrint('bindAlias failed: $e'); + } + } + + /// 解绑别名 + void unbindAlias(String alias, String sn, {bool isSelf = true}) { + try { + Getuiflut().unbindAlias(alias, sn, isSelf); + } catch (e) { + debugPrint('unbindAlias failed: $e'); + } + } + + /// 设置标签 + void setTags(List tags, String sn) { + try { + Getuiflut().setTag(tags, sn); + } catch (e) { + debugPrint('setTags failed: $e'); + } + } + + /// 静默时间(夜间免打扰)—— 实际 API 仅接受 beginHour + durationHours + void setSilentTime(int beginHour, int durationHours) { + try { + Getuiflut().setSilentTime(beginHour, durationHours); + } catch (e) { + debugPrint('setSilentTime failed: $e'); + } + } + + /// 角标清零 + void resetBadge() { + try { + Getuiflut().resetBadge(); + } catch (e) { + debugPrint('resetBadge failed: $e'); + } + } + + void dispose() { + _payloadController.close(); + _notificationController.close(); + } +} diff --git a/lib/service/update_service.dart b/lib/service/update_service.dart new file mode 100644 index 0000000..684e8e5 --- /dev/null +++ b/lib/service/update_service.dart @@ -0,0 +1,65 @@ +import 'dart:io'; +import 'package:autosos_flutter/api/user_api.dart'; +import 'package:autosos_flutter/config/constant.dart'; +import 'package:autosos_flutter/model/misc.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:install_plugin/install_plugin.dart'; +import 'package:open_filex/open_filex.dart'; +import 'package:path_provider/path_provider.dart'; + +/// 应用内更新服务(对应 Android HttpUpdateVersion) +class UpdateService { + static final UpdateService instance = UpdateService._(); + UpdateService._(); + + final _dio = Dio(); + + /// 检查更新 + Future checkUpdate() async { + try { + final resp = await UserApi.instance.checkUpdate(); + if (!resp.isSuccess) return null; + + final update = resp.data!; + Constant.NECUPDATE = update.forceUpdate ? 3 : 2; + return update; + } catch (e) { + debugPrint('checkUpdate failed: $e'); + Constant.NECUPDATE = 5; + return null; + } + } + + /// 下载并安装 + Future downloadAndInstall(AppUpdate update) async { + if (update.downloadUrl == null) { + EasyLoading.showError('下载链接为空'); + return; + } + EasyLoading.showProgress(0, status: '下载中 0%'); + + final dir = await getTemporaryDirectory(); + final apkPath = '${dir.path}/autosos_${update.latestVersion}.apk'; + + await _dio.download( + update.downloadUrl!, + apkPath, + onReceiveProgress: (received, total) { + if (total > 0) { + final p = received / total; + EasyLoading.showProgress(p, status: '下载中 ${(p * 100).toStringAsFixed(0)}%'); + } + }, + ); + + EasyLoading.dismiss(); + + if (Platform.isAndroid) { + await InstallPlugin.install(apkPath); + } else if (Platform.isIOS) { + await OpenFilex.open(apkPath); + } + } +} \ No newline at end of file diff --git a/lib/service/upload_service.dart b/lib/service/upload_service.dart new file mode 100644 index 0000000..9ac6f81 --- /dev/null +++ b/lib/service/upload_service.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:autosos_flutter/api/upload_api.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +/// 七牛文件上传服务(对应 Android QiNiuEntity + Qiniu SDK) +class UploadService { + static final UploadService instance = UploadService._(); + UploadService._(); + + final _dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 60), + )); + + /// 上传单个文件,返回远端 URL + Future upload(String localPath) async { + final tokenResp = await UploadApi.instance.qiNiuToken(); + if (!tokenResp.isSuccess) { + throw Exception('获取七牛凭证失败: ${tokenResp.message}'); + } + final token = tokenResp.data!; + + final form = FormData.fromMap({ + 'token': token.token, + 'file': await MultipartFile.fromFile(localPath), + }); + + final url = 'http://up.qiniup.com/'; // 七牛华东上传地址 + final r = await _dio.post(url, data: form, + options: Options(responseType: ResponseType.json)); + final data = r.data is Map ? r.data as Map : {}; + final key = data['key'] as String?; + if (key == null) throw Exception('七牛返回 key 为空'); + + final domain = token.domain.endsWith('/') ? token.domain : '${token.domain}/'; + return '$domain$key'; + } + + /// 批量上传(并发 3 个) + Future> uploadBatch(List localPaths, {int concurrency = 3}) async { + final results = []; + final pool = >[]; + + int cursor = 0; + Future worker() async { + while (cursor < localPaths.length) { + final i = cursor++; + try { + results[i] = await upload(localPaths[i]); + } catch (e) { + debugPrint('uploadBatch index=$i failed: $e'); + results[i] = null; + } + } + } + + for (int i = 0; i < concurrency && i < localPaths.length; i++) { + pool.add(worker()); + } + await Future.wait(pool); + + if (results.contains(null)) { + throw Exception('部分文件上传失败: $results'); + } + return results.cast(); + } + + /// 上传并校验文件存在 + Future uploadSafe(String localPath) async { + if (!await File(localPath).exists()) return null; + try { + return await upload(localPath); + } catch (e) { + debugPrint('uploadSafe failed: $e'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/util/toast.dart b/lib/util/toast.dart new file mode 100644 index 0000000..ecb1ca9 --- /dev/null +++ b/lib/util/toast.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +/// Toast 工具类(封装 fluttertoast + EasyLoading) +class Toast { + Toast._(); + + /// 短提示(屏幕底部) + static void show(String message) { + if (message.isEmpty) return; + Fluttertoast.showToast( + msg: message, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 2, + backgroundColor: const Color(0xCC000000), + textColor: Colors.white, + fontSize: 14, + ); + } + + /// 成功样式(顶部绿色弹条) + static void success(String message) { + if (message.isEmpty) return; + EasyLoading.showSuccess(message, duration: const Duration(seconds: 2)); + } + + /// 失败样式 + static void error(String message) { + if (message.isEmpty) return; + EasyLoading.showError(message, duration: const Duration(seconds: 2)); + } + + /// 警告 + static void warn(String message) { + if (message.isEmpty) return; + EasyLoading.showInfo(message, duration: const Duration(seconds: 2)); + } + + /// Loading + static void loading(String message) { + EasyLoading.show(status: message); + } + + static void dismiss() => EasyLoading.dismiss(); +} diff --git a/lib/util/xhttp.dart b/lib/util/xhttp.dart index 8022aca..298a9d1 100644 --- a/lib/util/xhttp.dart +++ b/lib/util/xhttp.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:autosos_flutter/config/constant.dart'; import 'package:autosos_flutter/util/device_id_utils.dart'; -import 'package:basic_utils/basic_utils.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; @@ -789,7 +788,6 @@ class Toast { // EasyLoading.instance.loadingStyle = EasyLoadingStyle.custom; // 此处可自定义风格 } - static final Toast _instance = Toast._(); static const String SUCCESS = "SUCCESS"; static const String ERROR = "ERROR"; diff --git a/lib/widget/multi_state_view.dart b/lib/widget/multi_state_view.dart new file mode 100644 index 0000000..3fb8713 --- /dev/null +++ b/lib/widget/multi_state_view.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +enum MultiState { loading, empty, error, success, offline } + +/// 多状态视图(对应 Android MultiStatePage) +class MultiStateView extends StatelessWidget { + final MultiState state; + final Widget? child; + final String? emptyText; + final String? errorText; + final VoidCallback? onRetry; + final Widget? loadingWidget; + + const MultiStateView({ + super.key, + required this.state, + this.child, + this.emptyText, + this.errorText, + this.onRetry, + this.loadingWidget, + }); + + @override + Widget build(BuildContext context) { + switch (state) { + case MultiState.loading: + return loadingWidget ?? + const Center(child: CircularProgressIndicator(strokeWidth: 2)); + case MultiState.empty: + return _Empty(text: emptyText ?? '暂无数据', onRetry: onRetry); + case MultiState.error: + return _Empty( + text: errorText ?? '加载失败,点击重试', + icon: Icons.error_outline, + onRetry: onRetry, + ); + case MultiState.offline: + return _Empty( + text: '当前无网络,请检查后重试', + icon: Icons.wifi_off, + onRetry: onRetry, + ); + case MultiState.success: + return child ?? const SizedBox.shrink(); + } + } +} + +class _Empty extends StatelessWidget { + final String text; + final IconData? icon; + final VoidCallback? onRetry; + + const _Empty({required this.text, this.icon, this.onRetry}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon ?? Icons.inbox_outlined, + size: 64, color: Colors.grey.shade400), + const SizedBox(height: 12), + Text(text, style: TextStyle(color: Colors.grey.shade600)), + if (onRetry != null) ...[ + const SizedBox(height: 16), + OutlinedButton(onPressed: onRetry, child: const Text('重试')), + ], + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widget/photo_grid_picker.dart b/lib/widget/photo_grid_picker.dart new file mode 100644 index 0000000..f416499 --- /dev/null +++ b/lib/widget/photo_grid_picker.dart @@ -0,0 +1,289 @@ +import 'dart:io'; +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:uuid/uuid.dart'; + +/// 自定义相机预览 + 拍照队列 +/// 对应 Android CheckPhotoActivity(多张拍照取证) +class CameraView extends StatefulWidget { + final void Function(String path) onCaptured; + const CameraView({super.key, required this.onCaptured}); + + @override + State createState() => _CameraViewState(); +} + +class _CameraViewState extends State + with WidgetsBindingObserver { + CameraController? _controller; + List _cameras = []; + int _cameraIdx = 0; + bool _initializing = true; + String? _error; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + _setup(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _controller?.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final c = _controller; + if (c == null || !c.value.isInitialized) return; + if (state == AppLifecycleState.inactive || + state == AppLifecycleState.paused) { + c.dispose(); + } else if (state == AppLifecycleState.resumed) { + _setup(); + } + } + + Future _setup() async { + setState(() { + _initializing = true; + _error = null; + }); + try { + final cam = await Permission.camera.request(); + if (!cam.isGranted) { + setState(() { + _initializing = false; + _error = '未授予相机权限'; + }); + return; + } + _cameras = await availableCameras(); + if (_cameras.isEmpty) { + setState(() { + _initializing = false; + _error = '未检测到摄像头'; + }); + return; + } + _cameraIdx = _cameras.indexWhere( + (c) => c.lensDirection == CameraLensDirection.back, + ); + if (_cameraIdx < 0) _cameraIdx = 0; + await _initController(); + } catch (e) { + setState(() { + _initializing = false; + _error = '相机初始化失败: $e'; + }); + } + } + + Future _initController() async { + final c = CameraController( + _cameras[_cameraIdx], + ResolutionPreset.high, + enableAudio: false, + ); + await c.initialize(); + if (!mounted) { + await c.dispose(); + return; + } + setState(() { + _controller = c; + _initializing = false; + }); + } + + Future _switchCamera() async { + if (_cameras.length < 2) return; + _cameraIdx = (_cameraIdx + 1) % _cameras.length; + await _controller?.dispose(); + await _initController(); + } + + Future _takePicture() async { + final c = _controller; + if (c == null || !c.value.isInitialized) return; + if (c.value.isTakingPicture) return; + try { + EasyLoading.show(status: '拍照中…'); + final xfile = await c.takePicture(); + EasyLoading.dismiss(); + // 复制到应用缓存目录(防止临时文件被清理) + final dir = await getTemporaryDirectory(); + final out = + '${dir.photos_cache_path()}/${const Uuid().v4()}.jpg'; + await File(xfile.path).copy(out); + widget.onCaptured(out); + } catch (e) { + EasyLoading.dismiss(); + EasyLoading.showError('拍照失败: $e'); + } + } + + @override + Widget build(BuildContext context) { + if (_error != null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.no_photography, size: 64, color: Colors.white), + const SizedBox(height: 8), + Text(_error!, style: const TextStyle(color: Colors.white)), + TextButton(onPressed: _setup, child: const Text('重试')), + ], + ), + ); + } + if (_initializing || _controller == null) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + return Stack( + fit: StackFit.expand, + children: [ + CameraPreview(_controller!), + // 顶部切换镜头 + Positioned( + top: 16, + right: 16, + child: SafeArea( + child: IconButton( + icon: const Icon(Icons.cameraswitch, color: Colors.white), + onPressed: _switchCamera, + ), + ), + ), + // 底部拍照按钮 + Positioned( + left: 0, + right: 0, + bottom: 32, + child: Center( + child: GestureDetector( + onTap: _takePicture, + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 4), + ), + child: const Center( + child: Icon(Icons.circle, color: Colors.white, size: 56), + ), + ), + ), + ), + ), + ], + ); + } +} + +extension _DirPath on Directory { + String photos_cache_path() { + final p = path; + if (p.endsWith('/')) return '${p}photos'; + return '$p/photos'; + } +} + +/// 底部照片缩略图横排(可删除) +class PhotoThumbStrip extends StatelessWidget { + final List paths; + final ValueChanged onRemove; + final VoidCallback? onAdd; + const PhotoThumbStrip({ + super.key, + required this.paths, + required this.onRemove, + this.onAdd, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 88, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + itemCount: paths.length + 1, + separatorBuilder: (_, __) => const SizedBox(width: 8), + itemBuilder: (ctx, i) { + if (i == paths.length) { + return InkWell( + onTap: onAdd, + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: Colors.black12, + borderRadius: BorderRadius.circular(6), + ), + child: const Icon(Icons.add_a_photo, color: Colors.white70), + ), + ); + } + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Image.file( + File(paths[i]), + width: 72, + height: 72, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => onRemove(i), + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(2), + child: const Icon( + Icons.close, + size: 14, + color: Colors.white, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +/// 备选:从相册选择(用 image_picker) +class AlbumPicker { + static final _picker = ImagePicker(); + + static Future> pick({int max = 9}) async { + final files = await _picker.pickMultiImage( + imageQuality: 85, + maxWidth: 1920, + ); + if (files.isEmpty) return const []; + return files.take(max).map((f) => f.path).toList(); + } +} diff --git a/lib/widget/qr_image.dart b/lib/widget/qr_image.dart new file mode 100644 index 0000000..5e45816 --- /dev/null +++ b/lib/widget/qr_image.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart' as qr; + +/// 二维码显示组件(避免和 flutter_inappwebview 包内同名类型冲突) +class QrImageView extends StatelessWidget { + final String data; + final double size; + final Color? backgroundColor; + + const QrImageView({ + super.key, + required this.data, + this.size = 200, + this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: backgroundColor ?? Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: qr.QrImageView( + data: data, + version: qr.QrVersions.auto, + size: size, + ), + ); + } +} diff --git a/lib/widget/section_header.dart b/lib/widget/section_header.dart new file mode 100644 index 0000000..feced66 --- /dev/null +++ b/lib/widget/section_header.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +/// 通用按钮(带 loading 态) +class LoadingButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final bool loading; + final Color? color; + final Color? textColor; + final double? width; + final double height; + final double radius; + + const LoadingButton({ + super.key, + required this.text, + this.onPressed, + this.loading = false, + this.color, + this.textColor, + this.width, + this.height = 48, + this.radius = 24, + }); + + @override + Widget build(BuildContext context) { + final c = color ?? Theme.of(context).primaryColor; + final tc = textColor ?? Colors.white; + return SizedBox( + width: width, + height: height, + child: ElevatedButton( + onPressed: loading ? null : onPressed, + style: ElevatedButton.styleFrom( + backgroundColor: c, + foregroundColor: tc, + disabledBackgroundColor: c.withValues(alpha: 0.5), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + ), + ), + child: loading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(tc), + ), + ) + : Text( + text, + style: TextStyle(fontSize: 16, color: tc, fontWeight: FontWeight.w600), + ), + ), + ); + } +} + +/// 通用 Section 头 +class SectionHeader extends StatelessWidget { + final String title; + final String? actionText; + final VoidCallback? onAction; + final EdgeInsetsGeometry padding; + + const SectionHeader({ + super.key, + required this.title, + this.actionText, + this.onAction, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + if (actionText != null) + GestureDetector( + onTap: onAction, + child: Text(actionText!, + style: TextStyle( + fontSize: 13, color: Theme.of(context).primaryColor)), + ), + ], + ), + ); + } +} + +/// 通用空状态 +class EmptyView extends StatelessWidget { + final String text; + final IconData icon; + const EmptyView({super.key, this.text = '暂无数据', this.icon = Icons.inbox_outlined}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 64, color: Colors.grey.shade400), + const SizedBox(height: 8), + Text(text, style: TextStyle(color: Colors.grey.shade600)), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widget/watermark.dart b/lib/widget/watermark.dart new file mode 100644 index 0000000..618eaa9 --- /dev/null +++ b/lib/widget/watermark.dart @@ -0,0 +1,80 @@ +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// 防截屏水印(对应 Android WatermarkUtils) +class Watermark extends StatelessWidget { + final String text; + final int columns; + final int rows; + final double rotation; + + const Watermark({ + super.key, + required this.text, + this.columns = 4, + this.rows = 8, + this.rotation = -0.4, + }); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: true, + child: CustomPaint( + painter: _WatermarkPainter( + text: '$text ${DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now())}', + columns: columns, + rows: rows, + rotation: rotation, + ), + child: const SizedBox.expand(), + ), + ); + } +} + +class _WatermarkPainter extends CustomPainter { + final String text; + final int columns; + final int rows; + final double rotation; + + _WatermarkPainter({ + required this.text, + required this.columns, + required this.rows, + required this.rotation, + }); + + @override + void paint(Canvas canvas, Size size) { + final textPainter = TextPainter( + text: TextSpan( + text: text, + style: TextStyle( + color: Colors.black.withValues(alpha: 0.06), + fontSize: 14, + ), + ), + textDirection: ui.TextDirection.ltr, + )..layout(); + + final dx = size.width / columns; + final dy = size.height / rows; + + for (int i = 0; i <= columns; i++) { + for (int j = 0; j <= rows; j++) { + canvas.save(); + canvas.translate(i * dx, j * dy); + canvas.rotate(rotation); + textPainter.paint(canvas, const Offset(0, 0)); + canvas.restore(); + } + } + } + + @override + bool shouldRepaint(_WatermarkPainter old) => old.text != text; +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 2e607a0..ee4cb0b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,12 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" amap_flutter_base: dependency: transitive description: name: amap_flutter_base sha256: "9ef2439b8de7100cdd1b4357701b8ca8c059c0f2d9d0257b81750bbf0c6f53bb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" amap_flutter_location: @@ -14,7 +22,7 @@ packages: description: name: amap_flutter_location sha256: f35ff00e196d579608e0bc28ccbc1f6f53787644702f947de941f775769cc701 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" amap_flutter_map: @@ -22,174 +30,646 @@ packages: description: name: amap_flutter_map sha256: "9cebb0b2f5fc7d3ae0427e99c41edc883e8f5459f6a28bc850f0f9e16918cf2f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.1" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.dev" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.dev" + source: hosted + version: "3.1.0" basic_utils: dependency: "direct main" description: name: basic_utils - sha256: "2064b21d3c41ed7654bc82cc476fd65542e04d60059b74d5eed490a4da08fc6c" - url: "https://pub.flutter-io.cn" + sha256: "548047bef0b3b697be19fa62f46de54d99c9019a69fb7db92c69e19d87f633c7" + url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.2" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + url: "https://pub.dev" + source: hosted + version: "4.1.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + camera: + dependency: "direct main" + description: + name: camera + sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8 + url: "https://pub.dev" + source: hosted + version: "0.10.6" + camera_android: + dependency: transitive + description: + name: camera_android + sha256: "8b3795d2f7f3c7e705821fe6cd82b7dd56c8a8e66f2df4ac6daf659876967dbf" + url: "https://pub.dev" + source: hosted + version: "0.10.11" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "11b4aee2f5e5e038982e152b4a342c749b414aa27857899d20f4323e94cb5f0b" + url: "https://pub.dev" + source: hosted + version: "0.9.23+2" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "4524ca6eb4176b066864036ad4fe02c3e4863e63b77eadc21a5bf56824f43498" + url: "https://pub.dev" + source: hosted + version: "2.13.1" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "1245a480a113437f8d46d19c0fb90cea9db921436d9cf2ba5fb11854a1312693" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.flutter-io.cn" + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.flutter-io.cn" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" + 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: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.flutter-io.cn" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.flutter-io.cn" + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.9" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" device_info_plus: dependency: "direct main" description: name: device_info_plus sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 - url: "https://pub.flutter-io.cn" + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.3" dio: dependency: "direct main" description: name: dio - sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" - url: "https://pub.flutter-io.cn" + sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0 + url: "https://pub.dev" source: hosted - version: "5.4.1" + version: "5.10.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4 + url: "https://pub.dev" + source: hosted + version: "2.2.0" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" - url: "https://pub.flutter-io.cn" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.flutter-io.cn" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.flutter-io.cn" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_background_service: + dependency: "direct main" + description: + name: flutter_background_service + sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + flutter_background_service_android: + dependency: transitive + description: + name: flutter_background_service_android + sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87 + url: "https://pub.dev" + source: hosted + version: "6.3.1" + flutter_background_service_ios: + dependency: "direct main" + description: + name: flutter_background_service_ios + sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_background_service_platform_interface: + dependency: transitive + description: + name: flutter_background_service_platform_interface + sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41 + url: "https://pub.dev" + source: hosted + version: "5.1.2" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_easyloading: dependency: "direct main" description: name: flutter_easyloading sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.5" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: e30fba942e3debea7b7e6cdd4f0f59ce89dd403a9865193e3221293b6d1544c6 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.flutter-io.cn" + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.0.2" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da - url: "https://pub.flutter-io.cn" + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.35" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" flutter_spinkit: dependency: transitive description: name: flutter_spinkit - sha256: b39c753e909d4796906c5696a14daf33639a76e017136c8d82bf3e620ce5bb8e - url: "https://pub.flutter-io.cn" + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -200,371 +680,1091 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" + fluwx: + dependency: "direct main" + description: + name: fluwx + sha256: d25c26a12a1fa280833105869ea255a5877695e03dc1190f0d2c027a7c989ef3 + url: "https://pub.dev" + source: hosted + version: "4.6.3" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" getuiflut: dependency: "direct main" description: name: getuiflut - sha256: "6bf5ac61fd92ef908455a23bc84e50946dd7316846706493b6638988b9d36d9e" - url: "https://pub.flutter-io.cn" + sha256: "3dbebe0928785ff8f8e0e409b959c6f7e7bdd83c17bd59aeae0cfac731717461" + url: "https://pub.dev" source: hosted - version: "0.2.25" + version: "0.2.41" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" http: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba - url: "https://pub.flutter-io.cn" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.flutter-io.cn" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + url: "https://pub.dev" + source: hosted + version: "4.9.1" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f + url: "https://pub.dev" + source: hosted + version: "0.8.13+17" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + install_plugin: + dependency: "direct main" + description: + name: install_plugin + sha256: "6fb67ba0781e75de4f2f2266ed25e835bfd277c5bfc2ed034af52774355857c6" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" js: dependency: transitive description: name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.flutter-io.cn" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 - url: "https://pub.flutter-io.cn" + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.flutter-io.cn" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.flutter-io.cn" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.flutter-io.cn" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" - url: "https://pub.flutter-io.cn" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e - url: "https://pub.flutter-io.cn" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: d234581c090526676fd8fab4ada92f35c6746e3fb4f05a399665d75a399fb760 + url: "https://pub.dev" + source: hosted + version: "5.2.3" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.flutter-io.cn" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" - url: "https://pub.flutter-io.cn" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" - url: "https://pub.flutter-io.cn" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" - url: "https://pub.flutter-io.cn" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" source: hosted - version: "12.0.5" + version: "12.1.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b - url: "https://pub.flutter-io.cn" + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" + url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "9.4.10" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" - url: "https://pub.flutter-io.cn" + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3+5" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" - url: "https://pub.flutter-io.cn" + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" platform: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" - url: "https://pub.flutter-io.cn" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.flutter-io.cn" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" source: hosted - version: "3.7.4" + version: "4.0.0" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sentry: + dependency: transitive + description: + name: sentry + sha256: "57514bc72d441ffdc463f498d6886aa586a2494fa467a1eb9d649c28010d7ee3" + url: "https://pub.dev" + source: hosted + version: "7.20.2" + sentry_dio: + dependency: "direct main" + description: + name: sentry_dio + sha256: aa7b9cebe479f143a0e302127d73d4086ef11ccfe21f0f4aa75e1139839402b9 + url: "https://pub.dev" + source: hosted + version: "7.20.2" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: "9723d58470ca43a360681ddd26abb71ca7b815f706bc8d3747afd054cf639ded" + url: "https://pub.dev" + source: hosted + version: "7.20.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" - url: "https://pub.flutter-io.cn" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.flutter-io.cn" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" - url: "https://pub.flutter-io.cn" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.flutter-io.cn" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" - url: "https://pub.flutter-io.cn" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" - url: "https://pub.flutter-io.cn" + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.flutter-io.cn" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + signature: + dependency: "direct main" + description: + name: signature + sha256: "8056e091ad59c2eb5735fee975ec649d0caf8ce802bb1ffb1e0955b00a6d0daa" + url: "https://pub.dev" + source: hosted + version: "5.5.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.10.0" - sprintf: + version: "1.10.2" + sqflite: dependency: transitive description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.flutter-io.cn" + name: sqflite + sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" + url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "2.4.2+1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" + url: "https://pub.dev" + source: hosted + version: "2.4.2+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.flutter-io.cn" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.flutter-io.cn" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + url: "https://pub.dev" + source: hosted + version: "3.4.0+1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.flutter-io.cn" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.10" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.flutter-io.cn" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" + url: "https://pub.dev" + source: hosted + version: "6.3.30" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + 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: "direct main" description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 - url: "https://pub.flutter-io.cn" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.6.0" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" + url: "https://pub.dev" + source: hosted + version: "1.2.6" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.flutter-io.cn" + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" - url: "https://pub.flutter-io.cn" + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.15.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" - url: "https://pub.flutter-io.cn" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00 + url: "https://pub.dev" + source: hosted + version: "0.5.2" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.flutter-io.cn" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: - dart: ">=3.2.0 <=3.2.3" - flutter: ">=3.16.0" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index fd3d49f..8db6180 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,103 +1,110 @@ name: autosos_flutter -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 +description: "JJSOS 道路救援技师端 — Flutter 实现,对齐 Android jjsos_autosos 4.1.3.3" +publish_to: 'none' +version: 4.1.39+421 environment: sdk: '>=3.0.0 <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 + # ============ 基础 ============ + cupertino_icons: ^1.0.6 + + # ============ 状态管理 & 路由 ============ + flutter_riverpod: ^2.5.1 + riverpod_annotation: ^2.3.5 + go_router: ^14.2.0 + + # ============ 网络 ============ + dio: ^5.4.1 + basic_utils: ^5.7.0 + + # ============ 存储 ============ + shared_preferences: ^2.2.2 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + # ============ 设备信息 / 工具 ============ + device_info_plus: ^9.1.2 + package_info_plus: ^8.0.0 + uuid: ^4.3.3 + intl: ^0.19.0 + path_provider: ^2.1.2 + url_launcher: ^6.2.4 + + # ============ 定位 / 地图 / 导航 ============ amap_flutter_location: ^3.0.0 amap_flutter_map: ^3.0.0 - dio: ^5.4.1 - flutter_easyloading: ^3.0.5 - shared_preferences: ^2.2.2 - basic_utils: ^5.7.0 - uuid: ^4.3.3 - device_info_plus: ^9.1.2 + + # ============ 推送 ============ getuiflut: ^0.2.25 + + # ============ 权限 ============ permission_handler: ^11.3.0 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + # ============ UI 反馈 ============ + flutter_easyloading: ^3.0.5 + fluttertoast: ^8.2.5 + + # ============ 媒体:相机/图片/签名 ============ + camera: ^0.10.5+9 + image_picker: ^1.0.7 + flutter_image_compress: ^2.3.0 + signature: ^5.4.0 + + # ============ 扫码 ============ + mobile_scanner: ^5.0.0 + qr_flutter: ^4.1.0 + + # ============ WebView ============ + flutter_inappwebview: ^6.0.0 + + # ============ 图片加载 ============ + cached_network_image: ^3.3.1 + photo_view: ^0.15.0 + flutter_svg: ^2.0.9 + shimmer: ^3.0.0 + + # ============ 音频(订单语音提醒) ============ + audioplayers: ^5.2.1 + + # ============ 后台服务 / 保活 ============ + flutter_background_service: ^5.0.5 + flutter_background_service_ios: ^5.0.0 + workmanager: ^0.5.1 + + # ============ 微信登录/支付/分享 ============ + fluwx: ^4.5.0 + + # ============ 崩溃监控 & 性能 ============ + sentry_flutter: ^7.13.0 + sentry_dio: ^7.13.0 + + # ============ 应用内更新(下载 APK) ============ + install_plugin: ^2.0.0 + open_filex: ^4.3.4 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: ^2.0.0 + 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 + # 代码生成 + build_runner: ^2.4.7 + json_serializable: ^6.7.1 + riverpod_generator: ^2.4.0 + hive_generator: ^2.0.1 -# 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/ - - images/1.5x/ - - images/2.0x/ - - images/3.0x/ - - images/4.0x/ - - # 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 + - images/ + - images/1.5x/ + - images/2.0x/ + - images/3.0x/ + - images/4.0x/ \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart index e40b85b..b1467f4 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,22 @@ -// 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. +// This is a basic Flutter widget test for jjsos_autosos_flutter. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:autosos_flutter/main.dart'; +import 'package:autosos_flutter/app.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); + testWidgets('App boots and shows splash/welcome', + (WidgetTester tester) async { + await tester.pumpWidget( + const ProviderScope( + child: AutososApp(), + ), + ); + // Allow first frame + await tester.pump(const Duration(milliseconds: 100)); + // No exception means the app rendered + expect(find.byType(MaterialApp), findsOneWidget); }); }