diff --git a/assets/images/yingyong11.png b/assets/images/yingyong11.png new file mode 100644 index 0000000..1dc8627 Binary files /dev/null and b/assets/images/yingyong11.png differ diff --git a/assets/route/routes.txt b/assets/route/routes.txt new file mode 100644 index 0000000..37de48d --- /dev/null +++ b/assets/route/routes.txt @@ -0,0 +1,581 @@ +{ + "extValues": {}, + "success": true, + "errCode": null, + "errMessage": null, + "exception": null, + "traceId": "18491030037470208", + "data": [ + { + "extValues": {}, + "id": "2030925300149387265", + "menuName": "首页", + "menuUrl": "/dashboard", + "parentId": "0", + "parentIds": null, + "menuPerms": "dashboard", + "menuType": 1, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 0, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387268", + "menuName": "单位管理", + "menuUrl": "/dashboard/Unit/Management", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Unit-Management", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 3, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387286", + "menuName": "就职单位", + "menuUrl": "/dashboard/Unit/Management/Employment/Unit", + "parentId": "2030925300149387268", + "parentIds": null, + "menuPerms": "dashboard-Unit-Management-Employment-Unit", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 21, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387287", + "menuName": "服务单位管理", + "menuUrl": "/dashboard/Unit/Management/Managee/Service/Unit/Management", + "parentId": "2030925300149387268", + "parentIds": null, + "menuPerms": "dashboard-Unit-Management-Managee-Service-Unit-Management", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 22, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387269", + "menuName": "通知公告", + "menuUrl": "/dashboard/Notice/Announcement", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Notice-Announcement", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 4, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387270", + "menuName": "口门门禁", + "menuUrl": "/dashboard/Gate/Access/Control", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Gate-Access-Control", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 5, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387288", + "menuName": "进港口门申请", + "menuUrl": "/dashboard/Gate/Access/Control/Port/Gate/Entry/Application", + "parentId": "2030925300149387270", + "parentIds": null, + "menuPerms": "dashboard-Gate-Access-Control-Port-Gate-Entry-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 23, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387292", + "menuName": "人员申请", + "menuUrl": "/dashboard/Port/Gate/Entry/Application/Personnel/Application", + "parentId": "2030925300149387288", + "parentIds": null, + "menuPerms": "dashboard-Port-Gate-Entry-Application-Personnel-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 27, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387293", + "menuName": "车辆申请", + "menuUrl": "/dashboard/Port/Gate/Entry/Application/Vehicle/Application", + "parentId": "2030925300149387288", + "parentIds": null, + "menuPerms": "dashboard-Port-Gate-Entry-Application-Vehicle-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 28, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387289", + "menuName": "进港口门申请记录", + "menuUrl": "/dashboard/Gate/Access/Crdtrol/Port/Gate/Entry/Record", + "parentId": "2030925300149387270", + "parentIds": null, + "menuPerms": "dashboard-Gate-Access-Crdtrol-Port-Gate-Entry-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 24, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387294", + "menuName": "人员申请记录", + "menuUrl": "/dashboard/Port/Gate/Entry/Record/Record/Personnel/Application/Record", + "parentId": "2030925300149387289", + "parentIds": null, + "menuPerms": "dashboard-Port-Gate-Entry-Record-Record-Personnel-Application-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 29, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387295", + "menuName": "车辆申请记录", + "menuUrl": "/dashboard/Port/Gate/Entry/Record/Record/Vehicle/Application/Record", + "parentId": "2030925300149387289", + "parentIds": null, + "menuPerms": "dashboard-Port-Gate-Entry-Record-Record-Vehicle-Application-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 30, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387290", + "menuName": "封闭区域口门申请", + "menuUrl": "/dashboard/Area/Access/Cionsedrol/Closed/Area/Gate/Application", + "parentId": "2030925300149387270", + "parentIds": null, + "menuPerms": "dashboard-Area-Access-Cionsedrol-Closed-Area-Gate-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 25, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387296", + "menuName": "人员申请", + "menuUrl": "/dashboard/Closed/Area/Gate/Application/Personnel/Application", + "parentId": "2030925300149387290", + "parentIds": null, + "menuPerms": "dashboard-Closed-Area-Gate-Application-Personnel-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 31, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387297", + "menuName": "车辆申请", + "menuUrl": "/dashboard/Closed/Area/Gate/Application/Vehicle/Application", + "parentId": "2030925300149387290", + "parentIds": null, + "menuPerms": "dashboard-Closed-Area-Gate-Application-Vehicle-Application", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 32, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387291", + "menuName": "封闭区域口门申请记录", + "menuUrl": "/dashboard/Area/Access/Crdsedrol/Closed/Area/Gate/Record", + "parentId": "2030925300149387270", + "parentIds": null, + "menuPerms": "dashboard-Area-Access-Crdsedrol-Closed-Area-Gate-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 26, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387298", + "menuName": "人员申请记录", + "menuUrl": "/dashboard/Closed/Area/Gate/Record/Record/Personnel/Application/Record", + "parentId": "2030925300149387291", + "parentIds": null, + "menuPerms": "dashboard-Closed-Area-Gate-Record-Record-Personnel-Application-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 33, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387299", + "menuName": "车辆申请记录", + "menuUrl": "/dashboard/Closed/Area/Gate/Record/Record/Vehicle/Application/Record", + "parentId": "2030925300149387291", + "parentIds": null, + "menuPerms": "dashboard-Closed-Area-Gate-Record-Record-Vehicle-Application-Record", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 34, + "showFlag": 1, + "children": null + } + ] + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387271", + "menuName": "现场监管", + "menuUrl": "/dashboard/Site/Supervision", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Site-Supervision", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 6, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387272", + "menuName": "危险作业", + "menuUrl": "/dashboard/Hazardous/Work", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Hazardous-Work", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 7, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387273", + "menuName": "隐患治理", + "menuUrl": "/dashboard/Hazard/Management", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Hazard-Management", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 8, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030935458707537921", + "menuName": "入港培训", + "menuUrl": "/dashboard/Study/Training", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-Study-Training", + "menuType": 1, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 11, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387300", + "menuName": "首页扫码", + "menuUrl": "/dashboard/scan", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-scan", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 35, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387301", + "menuName": "首页滚动通知", + "menuUrl": "/dashboard/roll-notice", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-roll-notice", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 36, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387302", + "menuName": "首页待办梳理", + "menuUrl": "/dashboard/todo-sort", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-todo-sort", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 37, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387303", + "menuName": "首页待办事项", + "menuUrl": "/dashboard/todo-list", + "parentId": "2030925300149387265", + "parentIds": null, + "menuPerms": "dashboard-todo-list", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 38, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387266", + "menuName": "通知", + "menuUrl": "/notice", + "parentId": "0", + "parentIds": null, + "menuPerms": "notice", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 1, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387274", + "menuName": "公告通知", + "menuUrl": "/notice/Announcement/Notice", + "parentId": "2030925300149387266", + "parentIds": null, + "menuPerms": "notice-Announcement-Notice", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 9, + "showFlag": 1, + "children": null + } + ] + }, + { + "extValues": {}, + "id": "2030925300149387267", + "menuName": "我的", + "menuUrl": "/my-center", + "parentId": "0", + "parentIds": null, + "menuPerms": "my-center", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 2, + "showFlag": 1, + "children": [ + { + "extValues": {}, + "id": "2030925300149387275", + "menuName": "我的信息", + "menuUrl": "/my-center/My/Information", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-My-Information", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 10, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387276", + "menuName": "扫码入职", + "menuUrl": "/my-center/Scan/Code/Onboarding", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Scan-Code-Onboarding", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 11, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387277", + "menuName": "人脸认证", + "menuUrl": "/my-center/Face/Authentication", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Face-Authentication", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 12, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387278", + "menuName": "证书信息", + "menuUrl": "/my-center/Certificate/Information", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Certificate-Information", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 13, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387279", + "menuName": "问题反馈", + "menuUrl": "/my-center/Feedback", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Feedback", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 14, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387280", + "menuName": "版本更新", + "menuUrl": "/my-center/Version/Update", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Version-Update", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 15, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387281", + "menuName": "关于我们", + "menuUrl": "/my-center/About/Us", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-About-Us", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 16, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387282", + "menuName": "切换账号", + "menuUrl": "/my-center/Switch/Account", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Switch-Account", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 17, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387283", + "menuName": "修改密码", + "menuUrl": "/my-center/Change/Password", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Change-Password", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 18, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387284", + "menuName": "用户注销", + "menuUrl": "/my-center/User/Logout", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-User-Logout", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 19, + "showFlag": 1, + "children": null + }, + { + "extValues": {}, + "id": "2030925300149387285", + "menuName": "退出登录", + "menuUrl": "/my-center/Logout", + "parentId": "2030925300149387267", + "parentIds": null, + "menuPerms": "my-center-Logout", + "menuType": 2, + "menuAttribution": "QINGANG_RELATED_PARTIES", + "sort": 20, + "showFlag": 1, + "children": null + } + ] + } + ], + "notEmpty": true, + "empty": false +} \ No newline at end of file diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart index 6297a60..1e428ff 100644 --- a/lib/http/ApiService.dart +++ b/lib/http/ApiService.dart @@ -9,19 +9,19 @@ class ApiService { static final bool isProduct = true; /// 登录及其他管理后台接口 - // static final String basePath = "http://192.168.198.8:30140"; - static final String basePath = - isProduct - ? "https://gbs-gateway.qhdsafety.com" - : "http://192.168.20.100:30140"; + static final String basePath = "http://192.168.198.8:30140"; + // static final String basePath = + // isProduct + // ? "https://gbs-gateway.qhdsafety.com" + // : "http://192.168.20.100:30140"; /// 图片文件服务 - static final String baseImgPath = - isProduct - ? "https://jpfz.qhdsafety.com/gbsFileTest/" - : "http://192.168.20.240:9787/mnt/"; //内网图片地址 - // static final String baseImgPath = "https://skqhdg.porthebei.com:9004/file/"; + // static final String baseImgPath = + // isProduct + // ? "https://jpfz.qhdsafety.com/gbsFileTest/" + // : "http://192.168.20.240:9787/mnt/"; //内网图片地址 + static final String baseImgPath = "https://skqhdg.porthebei.com:9004/file/"; static const publicKey = diff --git a/lib/http/modules/edu_api.dart b/lib/http/modules/edu_api.dart index e06e0e8..f246a8c 100644 --- a/lib/http/modules/edu_api.dart +++ b/lib/http/modules/edu_api.dart @@ -57,7 +57,7 @@ class EduApi { static Future> getSignInList(Map data) async { return HttpManager().request( '${ApiService.basePath}/edu', - '/app/studentSign/listAll', + '/app/studentSign/listAllNoGroup', method: Method.post, data: { ...data, diff --git a/lib/main.dart b/lib/main.dart index f627a94..046ab4f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -70,6 +70,7 @@ Future showModalBottomSheetAfterUnfocus({ void main( ) async { WidgetsFlutterBinding.ensureInitialized(); StorageService.instance.init(); + /** // 1) 同意 SDK 隐私(百度 SDK 要求) BMFMapSDK.setAgreePrivacy(true); @@ -82,7 +83,7 @@ void main( ) async { BMFMapSDK.setApiKeyAndCoordType('43G1sKuHV6oRTrdR9VTIGPF9soej7V5a', BMF_COORD_TYPE.BD09LL); await BMFAndroidVersion.initAndroidVersion(); // 可选,插件示例中有 } - +*/ await SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, diff --git a/lib/pages/home/Study/study_class_list_page.dart b/lib/pages/home/Study/study_class_list_page.dart index 6ec2b02..9319241 100644 --- a/lib/pages/home/Study/study_class_list_page.dart +++ b/lib/pages/home/Study/study_class_list_page.dart @@ -13,6 +13,7 @@ import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; import 'package:qhd_prevention/pages/mine/mine_sign_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/services/scan_service.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; @@ -146,6 +147,7 @@ class _StudyClassListPageState extends State { await pushPage(SigninInformationListPage(info: item), context); }, ), + if (item['examination'] != 0) CustomButton( text: '考试记录', height: 35, @@ -225,7 +227,11 @@ class _StudyClassListPageState extends State { child: Center(child: CircularProgressIndicator()), ); } - return _buildListItem(list[index]); + final item = list[index]; + if (item['state'] != 1) { + return _buildListItem(item); + } + return SizedBox(height: 0); }, ); } @@ -237,68 +243,7 @@ class _StudyClassListPageState extends State { ToastUtil.showNormal(context, '未扫描到二维码'); return; } - - int type = result['type'] ?? 0; - - final data = { - ...result, - 'phone': SessionService.instance.userData?.phone ?? '', - 'type': type, - }; - LoadingDialogHelper.show(); - - // 验证是否可以签到 - final response = await EduApi.checkSignIn(data); - LoadingDialogHelper.hide(); - - - if (response['success']) { - // 进行人脸识别 - final filePath = await pushPage( - const FaceRecognitionPage( - studentId: '', - data: {}, - mode: FaceMode.study, - ), - context, - ); - - final faceData = response['data']; - - if (filePath != null) { - // 对比人脸 - try { - LoadingDialogHelper.show(); - final response = await EduApi.compareFace({ - 'type': data['type'], - 'studentId': faceData['studentId'], - }, filePath); - final faceResultData = response['data']; - if (response['success']) { - final signData = { - 'id': faceResultData['id'] ?? '', - 'studentId': faceResultData['studentId'] ?? '', - 'type': data['type']?? '', - 'classId': faceResultData['classId'] ?? '', - 'studentSignId':faceResultData['studentSignId']??'' - }; - _signUpload(signData, type); - } else { - LoadingDialogHelper.hide(); - ToastUtil.showNormal(context, response['errMessage'] ?? '验证失败'); - } - } catch (e) { - ToastUtil.showNormal(context,'验证失败'); - LoadingDialogHelper.hide(); - print(e); - } - } else { - LoadingDialogHelper.hide(); - ToastUtil.showNormal(context, '签到失败'); - } - } else { - ToastUtil.showNormal(context, response['errMessage'] ?? '签到失败'); - } + ScanService.scan(context, result); } // 上传签到签字 diff --git a/lib/pages/home/Study/study_take_exam_page.dart b/lib/pages/home/Study/study_take_exam_page.dart index 1232fb7..d59c6ba 100644 --- a/lib/pages/home/Study/study_take_exam_page.dart +++ b/lib/pages/home/Study/study_take_exam_page.dart @@ -244,7 +244,7 @@ class _StudyTakeExamPageState extends State { final score = data['examScore'] ?? 0; var passed = data['result'] == 1; // 剩余考试次数 - final remain = data['surplusExamNum'] ?? 0 <= 0; + bool remain = (data['surplusExamNum'] ?? 0) <= 0; // 弹窗告诉用户结果:通过直接返回上一页;未通过给“继续考试 / 确定”两个按钮 final result = await CustomAlertDialog.showConfirm( @@ -267,7 +267,7 @@ class _StudyTakeExamPageState extends State { _resetForRetry(); } else { // 可选:处理失败情况并提示错误信息 - final msg = res['message'] ?? '提交失败,请重试'; + final msg = res['errMessage'] ?? '提交失败,请重试'; ToastUtil.showError(context, msg); } } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 88708c9..c43b26f 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,12 +1,13 @@ +// home_page.dart (适配新菜单配置) import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/common/route_aware_state.dart'; import 'package:qhd_prevention/common/route_service.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/pages/home/Study/study_tab_list_page.dart'; import 'package:qhd_prevention/pages/home/scan_page.dart'; @@ -16,6 +17,7 @@ import 'package:qhd_prevention/pages/mine/onboarding_full_page.dart'; import 'package:qhd_prevention/pages/user/choose_userFirm_page.dart'; import 'package:qhd_prevention/pages/user/firm_list_page.dart'; import 'package:qhd_prevention/services/auth_service.dart'; +import 'package:qhd_prevention/services/scan_service.dart'; import 'package:qhd_prevention/tools/h_colors.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,36 +34,34 @@ class HomePageState extends RouteAwareState with WidgetsBindingObserver, SingleTickerProviderStateMixin { final PageController _pageController = PageController(); final ScrollController _scrollController = ScrollController(); + bool _isShowCheckLogin = false; int _currentPage = 0; - bool _isMobileSelected = true; // 切换按钮状态 + bool _isMobileSelected = true; void startScan() async { - final result = await pushPage( ScanPage(type: ScanType.Onboarding), context, ); - if (result == null) { - return; - } - pushPage(OnboardingFullPage(scanData: result), context); + if (result == null) return; + ScanService.scan(context, result); } - // 缓存 key static const String _hiddenCacheKey = 'hidden_roll_cache'; - // 上面按钮显示状态 + // 上面按钮显示状态(与 buttonInfos 顺序对应) List _buttonVisibility = []; - // 我的工作子项显示状态 - List _workItemVisibility = []; + // 页面模块可见性(由路由驱动) + bool _showNotificationBar = true; + bool _showWorkStats = true; + bool _showCheckList = true; + List totalList = []; - // 工作统计数据 Map workStats = {'total': 36, 'processed': 30, 'pending': 6}; - // 检查清单数据 List> checkLists = [ { "title": "电工班车间清单", @@ -77,43 +77,12 @@ class HomePageState extends RouteAwareState }, { "title": "消防专项检查清单", - "type": "安环检查", + "type": "重点作业", "time": "2025-11-17", "color": Color(0xFFFF9800), }, - { - "title": "二车间防护网", - "type": "隐患处理", - "time": "2025-11-17", - "color": Color(0xFFF44336), - }, - { - "title": "二车间防护网", - "type": "隐患处理", - "time": "2025-11-17", - "color": Color(0xFFF44336), - }, - { - "title": "二车间防护网", - "type": "隐患处理", - "time": "2025-11-17", - "color": Color(0xFFF44336), - }, - { - "title": "二车间防护网", - "type": "隐患处理", - "time": "2025-11-17", - "color": Color(0xFFF44336), - }, - { - "title": "二车间防护网", - "type": "隐患处理", - "time": "2025-11-17", - "color": Color(0xFFF44336), - }, ]; - // 通知相关 final List _notifications = [ "系统通知:今晚20:00 将进行系统维护,请提前保存数据。", "安全提示:施工区请佩戴安全帽并系好安全带。", @@ -124,63 +93,54 @@ class HomePageState extends RouteAwareState late final PageController _notifPageController; Timer? _notifTimer; + // 图标按钮定义(顺序固定) List> buttonInfos = [ {"icon": "assets/images/ico1.png", "title": "单位管理", "unreadCount": 0}, {"icon": "assets/images/ico2.png", "title": "现场监管", "unreadCount": 0}, {"icon": "assets/images/ico3.png", "title": "危险作业", "unreadCount": 0}, - {"icon": "assets/images/ico4.png", "title": "隐患处理", "unreadCount": 0}, - {"icon": "assets/images/ico5.png", "title": "安环检查", "unreadCount": 0}, + {"icon": "assets/images/ico4.png", "title": "隐患治理", "unreadCount": 0}, // 原“隐患处理”改为“隐患治理” + {"icon": "assets/images/ico5.png", "title": "重点作业", "unreadCount": 0}, {"icon": "assets/images/ico6.png", "title": "口门门禁", "unreadCount": 0}, {"icon": "assets/images/ico7.png", "title": "入港培训", "unreadCount": 0}, ]; - // 浮动 AppBar(滚动触发) + // 浮动 AppBar bool _showFloatingAppBar = false; - static const double _triggerOffset = 30.0; // 滚动触发距离 + static const double _triggerOffset = 30.0; static const double _floatingBarHeight = 56.0; - // 更新模块和按钮显示状态的方法 - void _updateModuleAndButtonVisibility() { - final routeService = RouteService(); - final homeRoutes = - routeService.mainTabs.isNotEmpty - ? routeService.getRoutesForTab(routeService.mainTabs[0]) - : []; + // 按钮标题 -> 权限标识映射(基于新菜单配置) + final Map _titleToPerm = { + "单位管理": "dashboard-Unit-Management", + "现场监管": "dashboard-Site-Supervision", + "危险作业": "dashboard-Hazardous-Work", + "隐患治理": "dashboard-Hazard-Management", + "重点作业": "", // 无对应,暂时留空 + "口门门禁": "dashboard-Gate-Access-Control", + "入港培训": "dashboard-Study-Training", + }; - setState(() { - _buttonVisibility = List.filled(buttonInfos.length, false); - - // 根据路由标题匹配并设置显示状态(目前保留) - for (final route in homeRoutes) { - final routeTitle = route.title ?? ''; - } - }); - } - - /// 隐患播报列表及状态 - List> hiddenList = []; - bool _initialLoadingHidden = true; - bool _firstLoad = false; - - /// 记录显示登录弹窗状态 - bool _isShowCheckLogin = false; + // 模块权限映射(基于新菜单配置) + final Map _modulePerms = { + "notification": "dashboard-roll-notice", + "todoStats": "dashboard-todo-sort", + "checklist": "dashboard-todo-list", + "scan": "dashboard-scan", + }; @override void initState() { super.initState(); _isShowCheckLogin = widget.isChooseFirm; - // _getNeedSafetyCommitment(); - _buttonVisibility = List.filled(buttonInfos.length, true); - // 通知滚动 PageController + // 初始按钮全隐藏,避免闪烁 + _buttonVisibility = List.filled(buttonInfos.length, false); + _notifPageController = PageController(initialPage: 0); - - // 启动定时器:每 3 秒切换一条通知 _notifTimer = Timer.periodic(const Duration(seconds: 3), (timer) { if (!mounted) return; final next = (_notifIndex + 1) % _notifications.length; _notifIndex = next; - // animateToPage 可能抛异常(在 dispose 中),所以包在 try/catch try { _notifPageController.animateToPage( next, @@ -189,25 +149,73 @@ class HomePageState extends RouteAwareState ); } catch (_) {} _getNeedSafetyCommitment(); - // 获取待办事项 setState(() {}); }); - // 监听主列表滚动以控制浮动 AppBar 显示 _scrollController.addListener(_onScroll); - WidgetsBinding.instance.addObserver(this); - Future.delayed(const Duration(seconds: 1), () { - _firstLoad = true; + RouteService().addListener(onRouteConfigLoaded); + Future.microtask(() { + _updateModuleAndButtonVisibility(); }); - } - /// 校验是否入职 - Future _getNeedSafetyCommitment() async { - if (_isShowCheckLogin) { + @override + void dispose() { + _notifTimer?.cancel(); + _notifPageController.dispose(); + _pageController.dispose(); + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + WidgetsBinding.instance.removeObserver(this); + try { + RouteService().removeListener(onRouteConfigLoaded); + } catch (_) {} + super.dispose(); + } + + void onRouteConfigLoaded() { + if (!mounted) return; + _updateModuleAndButtonVisibility(); + } + + /// 根据路由配置更新按钮和模块可见性 + void _updateModuleAndButtonVisibility() { + final routeService = RouteService(); + final mainTabs = routeService.mainTabs; + if (mainTabs.isEmpty) { + // 路由未加载,保持全隐藏 return; } + + // 更新按钮可见性 + final List newVisibility = List.filled(buttonInfos.length, false); + for (int i = 0; i < buttonInfos.length; i++) { + final title = buttonInfos[i]['title'] as String; + final perm = _titleToPerm[title]; + if (perm == null || perm.isEmpty) continue; + try { + newVisibility[i] = routeService.hasPerm(perm); + } catch (e) { + debugPrint('权限检查异常: $perm, $e'); + } + } + + // 更新模块可见性 + final bool showNotif = routeService.hasPerm(_modulePerms['notification']!); + final bool showTodo = routeService.hasPerm(_modulePerms['todoStats']!); + final bool showChecklist = routeService.hasPerm(_modulePerms['checklist']!); + + setState(() { + _buttonVisibility = newVisibility; + _showNotificationBar = showNotif; + _showWorkStats = showTodo; + _showCheckList = showChecklist; + }); + } + + Future _getNeedSafetyCommitment() async { + if (_isShowCheckLogin) return; final prefs = await SharedPreferences.getInstance(); final phone = prefs.getString('savePhone') ?? ''; final pwd = prefs.getString('savePass') ?? ''; @@ -217,14 +225,12 @@ class HomePageState extends RouteAwareState List firmList = resData['userCorpInfoCOList'] ?? []; if (firmList.isNotEmpty) { _isShowCheckLogin = true; - CustomAlertDialog.showAlert( context, title: '温馨提示', content: '您的入职申请已通过', confirmText: '立即入职', onConfirm: () async { - // 入职一个企业直接进行底座登录传入企业 Map data = {'unitId': firmList.first['corpinfoId'] ?? ''}; final res = await AuthService.gbsLogin(phone, pwd, data); LoadingDialogHelper.hide(); @@ -245,111 +251,50 @@ class HomePageState extends RouteAwareState } void _onScroll() { - final offset = - _scrollController.hasClients ? _scrollController.offset : 0.0; + final offset = _scrollController.hasClients ? _scrollController.offset : 0.0; final shouldShow = offset >= _triggerOffset; if (shouldShow != _showFloatingAppBar) { - setState(() { - _showFloatingAppBar = shouldShow; - }); + setState(() => _showFloatingAppBar = shouldShow); } } - @override - void dispose() { - _pageController.dispose(); - _notifTimer?.cancel(); - _notifPageController.dispose(); - - _scrollController.removeListener(_onScroll); - _scrollController.dispose(); - - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - @override Future onVisible() async { final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0; const myIndex = 0; - if (current != myIndex) { - return; - } - if (_firstLoad) {} - } - - void onRouteConfigLoaded() { - if (mounted) { - setState(() { - _updateModuleAndButtonVisibility(); - }); - } - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - // App 回到前台时刷新数据 - } - } - - void _onBadgeUpdated() { - if (mounted) { - setState(() {}); - } - } - - /// 更新按钮角标 - void _updateButtonBadges() { - setState(() { - // 可以在这里更新角标数据 - }); - } - - /// 从 SharedPreferences 读取缓存 - Future _loadHiddenCache() async { - try { - final prefs = await SharedPreferences.getInstance(); - final jsonStr = prefs.getString(_hiddenCacheKey); - if (jsonStr != null && jsonStr.isNotEmpty) { - final parsed = jsonDecode(jsonStr) as List; - final list = - parsed.map((e) => Map.from(e as Map)).toList(); - setState(() { - hiddenList = list; - _initialLoadingHidden = false; - }); - } else { - setState(() { - _initialLoadingHidden = true; - }); - } - } catch (e) { - debugPrint('加载 hidden cache 失败: $e'); - setState(() { - _initialLoadingHidden = true; - }); - } + if (current != myIndex) return; + // 可在此刷新角标等 } Future _onRefresh() async { - // 刷新数据 await Future.delayed(const Duration(seconds: 1)); } @override Widget build(BuildContext context) { - const double notificationHeight = 60.0; // 通知栏高度 + const double notificationHeightConst = 60.0; double bannerHeight = 738 / 1125 * screenWidth(context); - const double iconSectionHeight = 220.0; - const double iconOverlapBanner = 90.0; // 图标区覆盖 banner 的高度 - const double iconOverlapNotification = -10.0; // 图标区覆盖通知栏的高度 + // 计算可见按钮列表 + final visibleButtons = >[]; + for (int i = 0; i < buttonInfos.length; i++) { + if (i < _buttonVisibility.length && _buttonVisibility[i]) { + visibleButtons.add(buttonInfos[i]); + } + } + + final double iconSectionHeight = + visibleButtons.isEmpty ? 0.0 : (visibleButtons.length <= 4 ? 120.0 : 220.0); + + final double notificationHeight = + (_showNotificationBar && iconSectionHeight >= 0) ? notificationHeightConst : 0.0; + + final double extraSpacing = _showNotificationBar ? 60.0 : 12.0; final double stackBottom = - bannerHeight - iconOverlapBanner + iconSectionHeight + 60; - + bannerHeight - (iconSectionHeight * 0.4) + iconSectionHeight + extraSpacing; final double statusBar = MediaQuery.of(context).padding.top; - final double indicatorCoverHeight = statusBar + 80.0; // 80 可根据需要调整 + + final topPadding = Platform.isIOS ? 0 : 14; return PopScope( canPop: false, @@ -357,10 +302,9 @@ class HomePageState extends RouteAwareState extendBodyBehindAppBar: true, body: Stack( children: [ - RefreshIndicator( - backgroundColor: Colors.white, // <- 关键:把背景设为不透明 - color: Colors.blue, // 进度条颜色 + backgroundColor: Colors.white, + color: Colors.blue, onRefresh: _onRefresh, child: ListView( controller: _scrollController, @@ -372,7 +316,7 @@ class HomePageState extends RouteAwareState child: Stack( clipBehavior: Clip.none, children: [ - // Banner(顶部) + // Banner Positioned( top: 0, left: 0, @@ -381,101 +325,83 @@ class HomePageState extends RouteAwareState child: _buildBannerSection(bannerHeight), ), - // 通知栏( - Positioned( - left: 10, - right: 10, - top: - (bannerHeight - iconOverlapBanner) + - iconSectionHeight - - iconOverlapNotification, - height: notificationHeight, - child: _buildNotificationBar(notificationHeight - 2), - ), + // 通知栏 + if (_showNotificationBar) + Positioned( + left: 10, + right: 10, + top: (bannerHeight - (iconSectionHeight * 0.4)) + iconSectionHeight - 10, + height: notificationHeight, + child: _buildNotificationBar(notificationHeight - 2), + ), - // 图标区(覆盖 banner 底部 overlap) + // 图标区 Positioned( left: 10, right: 10, - top: bannerHeight - iconOverlapBanner, + top: bannerHeight - (iconSectionHeight * 0.4), height: iconSectionHeight, - child: _buildIconSection(context), + child: _buildIconSection(context, visibleButtons, iconSectionHeight), ), ], ), ), - if (widget.isChooseFirm) ...[ - // 工作统计区域 + + if (widget.isChooseFirm && _showWorkStats) ...[ Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 20, - ), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), child: _buildWorkStatsSection(), ), + ], - // 检查清单区域 + if (widget.isChooseFirm && _showCheckList) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: _buildCheckListSection(), ), - const SizedBox(height: 20), ], ], ), ), - // 浮动 AppBar(平滑出现/隐藏),放在 ListView 之上 + + // 浮动 AppBar Positioned( top: 0, left: 0, right: 0, child: AnimatedContainer( duration: const Duration(milliseconds: 0), - height: - _showFloatingAppBar ? (statusBar + _floatingBarHeight) : 0, + height: _showFloatingAppBar ? (statusBar + _floatingBarHeight) : 0, decoration: BoxDecoration( - color: - _showFloatingAppBar - ? h_AppBarColor() - : Colors.transparent, - boxShadow: - _showFloatingAppBar - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 6, - ), - ] - : [], + color: _showFloatingAppBar ? h_AppBarColor() : Colors.transparent, + boxShadow: _showFloatingAppBar + ? [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 6)] + : [], ), child: SafeArea( bottom: false, child: Opacity( opacity: _showFloatingAppBar ? 1.0 : 0.0, - child: SizedBox( - height: _floatingBarHeight, - child: const SizedBox(), - ), + child: SizedBox(height: _floatingBarHeight, child: const SizedBox()), ), ), ), ), + + // 顶部中央标题 Positioned( - top: statusBar + 14, + top: statusBar + topPadding, width: screenWidth(context), child: Center( child: Text( '秦港-相关方安全管理', - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w600, - color: Colors.white, - ), + style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Colors.white), ), ), ), - // 固定在最上层的图标(位于 AppBar 之上),保证它们不会随滚动移动 + + // 右上角图标(扫码、加入企业) _buildFixedTopIcons(context), ], ), @@ -487,45 +413,47 @@ class HomePageState extends RouteAwareState pushPage(FirmListPage(isBack: true), context); } - // 固定在屏幕右上角的图标(不会随页面滚动) Widget _buildFixedTopIcons(BuildContext context) { final double statusBar = MediaQuery.of(context).padding.top; - // 固定图标距离顶部的偏移(在 banner 内时可调小) final double topOffset = statusBar + 12; + final routeService = RouteService(); + final bool showScan = routeService.hasPerm(_modulePerms['scan']!); + final bool showJoin = true; // 加入企业图标始终显示(或根据业务决定) + + final List children = []; + if (showScan) { + children.add( + GestureDetector( + onTap: startScan, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + child: Image.asset("assets/icon-apps/home_saoyisao.png", width: 20, height: 20), + ), + ), + ); + } + if (showJoin) { + if (children.isNotEmpty) children.add(const SizedBox(width: 8)); + children.add( + GestureDetector( + onTap: _joinFirm, + child: Container( + width: 30, + height: 30, + alignment: Alignment.center, + child: Image.asset("assets/icon-apps/home_add.png", width: 20, height: 20), + ), + ), + ); + } + return Positioned( top: topOffset, right: 8, - child: Row( - children: [ - GestureDetector( - onTap: startScan, - child: Container( - width: 30, - height: 30, - alignment: Alignment.center, - child: Image.asset( - "assets/icon-apps/home_saoyisao.png", - width: 20, - height: 20, - ), - ), - ), - GestureDetector( - onTap: _joinFirm, - child: Container( - width: 30, - height: 30, - alignment: Alignment.center, - child: Image.asset( - "assets/icon-apps/home_add.png", - width: 20, - height: 20, - ), - ), - ), - ], - ), + child: Row(children: children), ); } @@ -535,170 +463,130 @@ class HomePageState extends RouteAwareState child: Container( height: notificationHeight, decoration: BoxDecoration( - color: const Color(0xFFE6F5FF), // 浅蓝 + color: const Color(0xFFE6F5FF), borderRadius: BorderRadius.circular(5), - border: Border.all(color: Colors.white, width: 1), // 白色边框 + border: Border.all(color: Colors.white, width: 1), boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 6, - offset: const Offset(0, 2), - ), + BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 6, offset: const Offset(0, 2)), ], ), - child: Column( + child: Row( children: [ - // const SizedBox(height: 40), - Row( - children: [ - const SizedBox(width: 12), - // 左侧图标 - Image.asset('assets/images/ico8.png', width: 30, height: 25), - const SizedBox(width: 12), - - // 中间可滚动的文本区域(使用垂直 PageView) - Expanded( - child: SizedBox( - height: notificationHeight, - child: PageView.builder( - controller: _notifPageController, - scrollDirection: Axis.vertical, - physics: const NeverScrollableScrollPhysics(), - itemCount: _notifications.length, - itemBuilder: (context, index) { - final text = _notifications[index]; - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Text( - text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.black87, - fontSize: 14, - ), - ), - ), - ); - }, - ), - ), + const SizedBox(width: 12), + Image.asset('assets/images/ico8.png', width: 30, height: 25), + const SizedBox(width: 12), + Expanded( + child: SizedBox( + height: notificationHeight, + child: PageView.builder( + controller: _notifPageController, + scrollDirection: Axis.vertical, + physics: const NeverScrollableScrollPhysics(), + itemCount: _notifications.length, + itemBuilder: (context, index) { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + _notifications[index], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.black87, fontSize: 14), + ), + ), + ); + }, ), - - // 右侧箭头 - const Icon(Icons.chevron_right, color: Colors.black26), - const SizedBox(width: 8), - ], + ), ), + const Icon(Icons.chevron_right, color: Colors.black26), + const SizedBox(width: 8), ], ), ), ); } - // 构建顶部 Banner Widget _buildBannerSection(double bannerHeight) { - return Stack( - children: [ - // 背景图片 - Image.asset( - "assets/images/banner.png", - width: MediaQuery.of(context).size.width, - height: bannerHeight, - fit: BoxFit.cover, - ), - - // 这里保留 banner 内的额外装饰(如果需要) - ], + return Image.asset( + "assets/images/banner.png", + width: MediaQuery.of(context).size.width, + height: bannerHeight, + fit: BoxFit.cover, ); } - Widget _buildIconSection(BuildContext context) { - // 判断是否有第二行 - final hasSecondRow = buttonInfos.length > 4; + Widget _buildIconSection(BuildContext context, List> visibleButtons, double height) { + if (RouteService().mainTabs.isNotEmpty && visibleButtons.isEmpty) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], + ), + child: const Center(child: Text('暂无权限访问的功能')), + ); + } + + final hasSecondRow = visibleButtons.length > 4; + final double containerMinHeight = height > 0 ? height : 120.0; return Container( padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + constraints: BoxConstraints(minHeight: containerMinHeight), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)), - ], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], + ), + child: widget.isChooseFirm + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: List.generate(4, (i) { + if (i < visibleButtons.length) { + return Expanded(child: Center(child: _buildIconButton(visibleButtons[i], context))); + } else { + return const Expanded(child: SizedBox()); + } + }), + ), + if (hasSecondRow) const SizedBox(height: 20), + if (hasSecondRow) + Row( + children: List.generate(4, (i) { + final idx = 4 + i; + if (idx < visibleButtons.length) { + return Expanded(child: Center(child: _buildIconButton(visibleButtons[idx], context))); + } else { + return const Expanded(child: SizedBox()); + } + }), + ), + ], + ) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/ico1.png', width: 100, height: 100), + const SizedBox(height: 10), + CustomButton( + text: '点击入职企业', + onPressed: () => pushPage(FirmListPage(isBack: true), context), + ), + ], + ), ), - child: - widget.isChooseFirm - ? Column( - children: [ - // 第一行(4 列等分) - Row( - children: List.generate(4, (i) { - final idx = i; - if (idx < buttonInfos.length) { - return Expanded( - child: Center( - child: _buildIconButton(buttonInfos[idx], context), - ), - ); - } else { - return const Expanded(child: SizedBox()); - } - }), - ), - - if (hasSecondRow) const SizedBox(height: 20), - - // 第二行(仍然 4 列等分;不足的用占位填充) - if (hasSecondRow) - Row( - children: List.generate(4, (i) { - final idx = 4 + i; // 第二行从索引4开始 - if (idx < buttonInfos.length) { - return Expanded( - child: Center( - child: _buildIconButton( - buttonInfos[idx], - context, - ), - ), - ); - } else { - return const Expanded(child: SizedBox()); - } - }), - ), - ], - ) - : Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - 'assets/images/ico1.png', - width: 100, - height: 100, - ), - const SizedBox(height: 10), - CustomButton( - text: '点击入职企业', - onPressed: () { - pushPage(FirmListPage(isBack: true), context); - }, - ), - ], - ), - ), ); } - // 构建单个图标按钮(保持原样) Widget _buildIconButton(Map info, BuildContext context) { return GestureDetector( - onTap: () { - _handleIconTap(info['title']); - }, + onTap: () => _handleIconTap(info['title']), child: Column( children: [ Container( @@ -708,9 +596,7 @@ class HomePageState extends RouteAwareState color: const Color(0xFFE8F4FD), borderRadius: BorderRadius.circular(25), ), - child: Center( - child: Image.asset(info['icon'], width: 30, height: 30), - ), + child: Center(child: Image.asset(info['icon'], width: 30, height: 30)), ), const SizedBox(height: 6), SizedBox( @@ -728,31 +614,20 @@ class HomePageState extends RouteAwareState ); } - // 处理图标点击 void _handleIconTap(String title) { switch (title) { case "单位管理": pushPage(UnitTabPage(), context); break; - case "现场监管": - break; - case "危险作业": - break; - case "隐患处理": - break; - case "安环检查": - break; - case "口门门禁": - break; case "入港培训": pushPage(StudyTabListPage(), context); break; default: + ToastUtil.showNormal(context, '功能开发中...'); break; } } - // 构建工作统计区域 - 新版设计 Widget _buildWorkStatsSection() { double buttonHeight = 45.0; return Container( @@ -760,52 +635,31 @@ class HomePageState extends RouteAwareState decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)), - ], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 切换按钮 Container( width: screenWidth(context), height: buttonHeight, - decoration: BoxDecoration( - // color: Colors.white, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - // 手机端按钮 Expanded( child: GestureDetector( - onTap: () { - setState(() { - _isMobileSelected = true; - }); - }, + onTap: () => setState(() => _isMobileSelected = true), child: Container( decoration: BoxDecoration( - color: - _isMobileSelected - ? const Color(0xFFE9F4FE) - : Colors.white, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - ), + color: _isMobileSelected ? const Color(0xFFE9F4FE) : Colors.white, + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12)), ), child: Center( child: Text( "手机端", style: TextStyle( fontSize: 14, - color: - _isMobileSelected ? Colors.black : Colors.blue, + color: _isMobileSelected ? Colors.black : Colors.blue, fontWeight: FontWeight.w500, ), ), @@ -819,31 +673,20 @@ class HomePageState extends RouteAwareState height: buttonHeight, width: 30, ), - // 电脑端按钮 Expanded( child: GestureDetector( - onTap: () { - setState(() { - _isMobileSelected = false; - }); - }, + onTap: () => setState(() => _isMobileSelected = false), child: Container( decoration: BoxDecoration( - color: - !_isMobileSelected - ? const Color(0xFFE9F4FE) - : Colors.white, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(12), - ), + color: !_isMobileSelected ? const Color(0xFFE9F4FE) : Colors.white, + borderRadius: const BorderRadius.only(topRight: Radius.circular(12)), ), child: Center( child: Text( "电脑端", style: TextStyle( fontSize: 15, - color: - !_isMobileSelected ? Colors.black : Colors.blue, + color: !_isMobileSelected ? Colors.black : Colors.blue, fontWeight: FontWeight.w500, ), ), @@ -855,95 +698,50 @@ class HomePageState extends RouteAwareState ), ), Padding( - padding: EdgeInsets.all(15), + padding: const EdgeInsets.all(15), child: Column( children: [ - // 今日工作项 Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 0), child: RichText( text: TextSpan( children: [ - TextSpan( - text: "今日有工作项", - style: TextStyle(fontSize: 16, color: Colors.black87), - ), + const TextSpan(text: "今日有工作项", style: TextStyle(fontSize: 16, color: Colors.black87)), TextSpan( text: " ${workStats['total']}", - style: const TextStyle( - fontSize: 16, - color: Color(0xFF2A75F8), - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: "个", - style: TextStyle(fontSize: 16, color: Colors.black87), + style: const TextStyle(fontSize: 16, color: Color(0xFF2A75F8), fontWeight: FontWeight.bold), ), + const TextSpan(text: "个", style: TextStyle(fontSize: 16, color: Colors.black87)), ], ), ), ), - const SizedBox(height: 15), - // 第三行:已处理和待处理工作项 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ RichText( text: TextSpan( children: [ - TextSpan( - text: "已处理工作项:", - style: TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), + const TextSpan(text: "已处理工作项:", style: TextStyle(fontSize: 14, color: Colors.black87)), TextSpan( text: " ${workStats['processed']}", - style: TextStyle( - fontSize: 14, - color: Colors.greenAccent, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: "个", - style: TextStyle( - fontSize: 14, - color: Colors.black87, - ), + style: const TextStyle(fontSize: 14, color: Colors.greenAccent, fontWeight: FontWeight.bold), ), + const TextSpan(text: "个", style: TextStyle(fontSize: 14, color: Colors.black87)), ], ), ), RichText( text: TextSpan( children: [ + const TextSpan(text: "待处理工作项:", style: TextStyle(fontSize: 14, color: Colors.black87)), TextSpan( - text: "待处理工作项:", - style: TextStyle( - fontSize: 14, - color: Colors.black87, - ), - ), - TextSpan( - text: " ${workStats['processed']}", - style: TextStyle( - fontSize: 14, - color: Colors.orange, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: "个", - style: TextStyle( - fontSize: 14, - color: Colors.black87, - ), + text: " ${workStats['pending']}", + style: const TextStyle(fontSize: 14, color: Colors.orange, fontWeight: FontWeight.bold), ), + const TextSpan(text: "个", style: TextStyle(fontSize: 14, color: Colors.black87)), ], ), ), @@ -957,42 +755,29 @@ class HomePageState extends RouteAwareState ); } - // 构建检查清单区域 Widget _buildCheckListSection() { return Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - ' 待办清单', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(), + children: const [ + Text(' 待办清单', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + SizedBox(), ], ), - SizedBox(height: 8), + const SizedBox(height: 8), Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - children: [...checkLists.map((item) => _buildCheckListItem(item))], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], ), + child: Column(children: [...checkLists.map((item) => _buildCheckListItem(item))]), ), ], ); } - // 构建检查清单项 Widget _buildCheckListItem(Map item) { return Container( padding: const EdgeInsets.all(15), @@ -1001,70 +786,48 @@ class HomePageState extends RouteAwareState ), child: Row( children: [ - // 内容区域 - 使用Expanded确保不溢出 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题行 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // 标题使用Flexible防止溢出 Flexible( child: Text( item['title'], - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, // 添加省略号 - maxLines: 1, // 限制为1行 + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black87), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), - const SizedBox(width: 8), // 添加间距 - Text( - item['type'], - style: TextStyle(fontSize: 12, color: Colors.grey[500]), - ), + const SizedBox(width: 8), + Text(item['type'], style: TextStyle(fontSize: 12, color: Colors.grey[500])), ], ), - const SizedBox(height: 8), // 替代Divider的间距 - // 底部信息行 + const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // 类型标签使用Flexible Flexible( child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(4), ), child: Text( "类型:${item['type']}", - style: const TextStyle( - fontSize: 12, - color: Colors.blue, - ), + style: const TextStyle(fontSize: 12, color: Colors.blue), overflow: TextOverflow.ellipsis, ), ), ), - const SizedBox(width: 8), // 添加间距 - // 时间使用Text + const SizedBox(width: 8), Flexible( child: Text( "时间:${item['time']}", - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), + style: const TextStyle(fontSize: 12, color: Colors.grey), overflow: TextOverflow.ellipsis, ), ), @@ -1077,4 +840,4 @@ class HomePageState extends RouteAwareState ), ); } -} +} \ No newline at end of file diff --git a/lib/pages/home/unit/unit_tab_page.dart b/lib/pages/home/unit/unit_tab_page.dart index 1a8ac31..5cf0c50 100644 --- a/lib/pages/home/unit/unit_tab_page.dart +++ b/lib/pages/home/unit/unit_tab_page.dart @@ -1,4 +1,7 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:qhd_prevention/common/route_model.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/customWidget/work_tab_icon_grid.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/customWidget/IconBadgeButton.dart'; @@ -6,6 +9,7 @@ import 'package:qhd_prevention/pages/home/unit/unit_join_list_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:qhd_prevention/common/route_aware_state.dart'; +import 'package:qhd_prevention/common/route_service.dart'; class UnitTabPage extends StatefulWidget { const UnitTabPage({super.key}); @@ -15,47 +19,166 @@ class UnitTabPage extends StatefulWidget { } class _UnitTabPageState extends RouteAwareState { - late List> buttonInfos = [ + // 原始 master 定义(固定顺序,不随权限变动) + final List> _masterButtons = [ { "icon": "assets/images/unit_ico1.png", "title": "服务单位管理", "unreadCount": 0, - } - ,{ + }, + { "icon": "assets/images/unit_ico2.png", "title": "就职单位管理", "unreadCount": 0, - } + }, ]; + // title -> 后端 menuPerm 映射(基于你提供的路由配置) + final Map _permMapping = { + "服务单位管理": "dashboard-Unit-Management-Managee-Service-Unit-Management", + "就职单位管理": "dashboard-Unit-Management-Employment-Unit", + }; + + // 当前哪些按钮应显示(与 master 顺序对应) + late List _visible; + @override void initState() { super.initState(); - } - Future onVisible() async { - _getData(); - } - Future _getData() async { + // 初始:全部显示,避免短暂白屏(路由加载后会更新) + _visible = List.filled(_masterButtons.length, true); + // 监听路由变化,路由加载完成或变更时刷新可见性 + RouteService().addListener(_onRouteUpdated); + + // 尝试一次应用路由(如果已经加载) + WidgetsBinding.instance.addPostFrameCallback((_) => _updateVisibilityFromRoute()); + } + + @override + void dispose() { + try { + RouteService().removeListener(_onRouteUpdated); + } catch (_) {} + super.dispose(); + } + + void _onRouteUpdated() { + // 当路由配置更新时重计算按钮可见性 + _updateVisibilityFromRoute(); + } + + void _updateVisibilityFromRoute() { + final routeService = RouteService(); + + // routesLoaded: 当 mainTabs 非空时认为路由加载完毕(开始严格按后端配置显示) + final bool routesLoaded = routeService.mainTabs.isNotEmpty; + + final List next = List.filled(_masterButtons.length, false); + + for (int i = 0; i < _masterButtons.length; i++) { + final title = _masterButtons[i]['title'] as String; + final perm = _permMapping[title] ?? ''; + + if (!routesLoaded) { + // 路由还没加载:保持默认显示(避免闪烁) + next[i] = true; + continue; + } + + if (perm.isEmpty) { + // 没有映射:默认隐藏 + next[i] = false; + continue; + } + + // 优先使用 RouteService.findRouteByPerm 获取路由节点(该方法会考虑节点 visible) + final RouteModel? node = routeService.findRouteByPerm(perm); + + if (node != null) { + // 按你的要求:以 showFlag == 1 判断是否显示(兼容旧字段) + next[i] = (node.showFlag == 1) || (node.visible); + } else { + // 如果没有找到完全匹配的 menuPerm,有可能后端使用不同层级或不同 perms 命名, + // 这里做一次容错:在所有顶级下递归查找 menuPerm 相等项(不依赖 findRouteByPerm)。 + RouteModel? fallback; + for (final top in routeService.allRoutes) { + fallback = _findRouteRecursiveByPerm(top, perm); + if (fallback != null) break; + } + next[i] = fallback != null ? ((fallback.showFlag == 1) || (fallback.visible)) : false; + } + } + + // 只有在发生变化时才 setState + if (!listEquals(next, _visible)) { + setState(() { + _visible = next; + }); + } + } + + /// 递归:在以 node 为根的子树中查找第一个 menuPerms 等于 targetPerm 的节点 + /// (不依赖 RouteService.findRouteByPerm,作为 fallback) + RouteModel? _findRouteRecursiveByPerm(RouteModel node, String targetPerm) { + if ((node.menuPerms ?? '') == targetPerm) return node; + for (final c in node.children) { + final res = _findRouteRecursiveByPerm(c, targetPerm); + if (res != null) return res; + } + return null; + } + + // 当页面可见时拉取数据 + @override + Future onVisible() async { + await _getData(); + } + + Future _getData() async { + // TODO: 如果需要从后端拉取角标/统计,在这里实现并 setState 更新 _masterButtons[*]['unreadCount'] + // await Future.delayed(Duration(milliseconds: 100)); } void _handleIconTap(int index) async { - switch (index) { - case 0: + final title = _masterButtons[index]['title'] as String; + switch (title) { + case '服务单位管理': + ToastUtil.showNormal(context, '您还没有参与项目'); break; - case 1: + case '就职单位管理': pushPage(UnitJoinListPage(), context); break; default: break; } - _getData(); + + // 点击后可以刷新数据 + await _getData(); } + @override Widget build(BuildContext context) { - double bannerHeight = 618/1125 * MediaQuery.of(context).size.width; - const double iconSectionHeight = 150.0; - const double iconOverlapBanner = 30.0; // 图标区覆盖 banner 的高度 + final double screenW = MediaQuery.of(context).size.width; + final double bannerHeight = 618 / 1125 * screenW; + + // 根据可见按钮数量决定 icon 区高度(每行最多 4 个) + final visibleButtons = >[]; + for (int i = 0; i < _masterButtons.length; i++) { + if (i < _visible.length && _visible[i]) visibleButtons.add(_masterButtons[i]); + } + final int visibleCount = visibleButtons.length; + final int perRow = 4; + final int rows = visibleCount == 0 ? 0 : ((visibleCount + perRow - 1) ~/ perRow); + + // 样式参数(如需微调) + const double verticalPadding = 30.0; + const double perRowHeight = 110.0; // 单行高度(图标 + 文本 + 内间距) + const double rowSpacing = 20.0; + final double iconSectionHeight = visibleCount == 0 ? 150.0 : (verticalPadding + rows * perRowHeight + (rows - 1) * rowSpacing); + + const double iconOverlapBanner = 30.0; + return PopScope( canPop: true, child: Scaffold( @@ -85,7 +208,7 @@ class _UnitTabPageState extends RouteAwareState { right: 10, top: bannerHeight - iconOverlapBanner, height: iconSectionHeight, - child: _buildIconSection(context), + child: _buildIconSection(context, visibleButtons, rows), ), ], ), @@ -94,13 +217,12 @@ class _UnitTabPageState extends RouteAwareState { ), ), ); - } - // 构建顶部 Banner + + // Banner Widget _buildBannerSection(double height) { return Stack( children: [ - // 背景图片 Image.asset( "assets/images/unit_banner.jpg", width: MediaQuery.of(context).size.width, @@ -110,54 +232,80 @@ class _UnitTabPageState extends RouteAwareState { ], ); } - Widget _buildIconSection(BuildContext context) { + + // 根据 visibleButtons 与行数构建 icon 区 + Widget _buildIconSection(BuildContext context, List> visibleButtons, int rows) { + if (visibleButtons.isEmpty) { + if (RouteService().mainTabs.isNotEmpty) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], + ), + child: const Center(child: Text('暂无权限访问的功能')), + ); + } else { + return Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], + ), + child: const Center(child: SizedBox()), + ); + } + } + + // 构建每一行(每行最多 4 个) + final List rowsWidgets = []; + final int perRow = 4; + for (int r = 0; r < rows; r++) { + final start = r * perRow; + final end = (start + perRow) > visibleButtons.length ? visibleButtons.length : (start + perRow); + final rowItems = visibleButtons.sublist(start, end); + + rowsWidgets.add( + Row( + children: List.generate(perRow, (i) { + final idx = start + i; + if (idx < visibleButtons.length) { + final btn = visibleButtons[idx]; + // 找到在 master 中的真实索引(用于 onTap) + final masterIndex = _masterButtons.indexWhere((m) => m['title'] == btn['title']); + return Expanded( + child: Center(child: _buildIconButton(btn, masterIndex)), + ); + } else { + return const Expanded(child: SizedBox.shrink()); + } + }), + ), + ); + + if (r != rows - 1) rowsWidgets.add(const SizedBox(height: 20)); + } + return Container( - padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 5), + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - _buildIconRow(startIndex: 0), - - ], + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2))], ), + child: Column(children: rowsWidgets), ); } - Widget _buildIconRow({required int startIndex}) { - final List cells = List.generate(4, (i) { - final int idx = startIndex + i; - if (idx < buttonInfos.length) { - return Expanded( - child: Center(child: _buildIconButton(buttonInfos[idx], idx, context)), - ); - } else { - return const Expanded( - child: SizedBox.shrink(), - ); - } - }); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: cells, - ); - } - Widget _buildIconButton(Map info, int index, BuildContext context) { + Widget _buildIconButton(Map info, int masterIndex) { + final unread = (info['unreadCount'] ?? 0) is int ? info['unreadCount'] as int : int.tryParse('${info['unreadCount']}') ?? 0; return IconBadgeButton( iconPath: info['icon'] ?? '', title: info['title'] ?? '', - unreadCount: (info['unreadCount'] ?? 0) is int ? info['unreadCount'] as int : int.tryParse('${info['unreadCount']}') ?? 0, - onTap: () => _handleIconTap(index), + unreadCount: unread, + onTap: () => _handleIconTap(masterIndex), ); } - -} +} \ No newline at end of file diff --git a/lib/pages/main_tab.dart b/lib/pages/main_tab.dart index 319d891..ef723a2 100644 --- a/lib/pages/main_tab.dart +++ b/lib/pages/main_tab.dart @@ -1,15 +1,22 @@ +// lib/pages/new/main_page.dart +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:qhd_prevention/common/route_service.dart'; +import 'package:qhd_prevention/http/modules/appmenu_api.dart'; import 'package:qhd_prevention/pages/badge_manager.dart'; import 'package:qhd_prevention/pages/home/home_page.dart'; -import 'package:qhd_prevention/pages/mine/mine_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/pages/notif/notif_page.dart'; import 'package:qhd_prevention/services/heartbeat_service.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +import 'mine/mine_page.dart'; /// 用于向子树公布当前 tab 索引 class CurrentTabNotifier extends InheritedWidget { final int currentIndex; - const CurrentTabNotifier({ required this.currentIndex, required Widget child, @@ -38,10 +45,14 @@ class _MainPageState extends State with WidgetsBindingObserver { int _currentIndex = 0; final GlobalKey _homeKey = GlobalKey(); final GlobalKey _notifKey = GlobalKey(); - // final GlobalKey _appKey = GlobalKey(); final GlobalKey _mineKey = GlobalKey(); - late List _pages; - late List _tabVisibility; // 存储每个Tab的显示状态 + + // 固定页面顺序(不要修改顺序,否则对应 _tabVisibility 的索引会错) + late final List _pages; + + // 存储每个固定Tab是否可见(对应 _pages 的顺序) + // [首页, 通知, 我的] + late List _tabVisibility; // 添加 BadgeManager 实例 late final BadgeManager _badgeManager; @@ -49,53 +60,156 @@ class _MainPageState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); // 初始化 BadgeManager _badgeManager = BadgeManager(); _badgeManager.initAllModules(); - - // 监听 BadgeManager 的变化 _badgeManager.addListener(_onBadgeChanged); - // 注册生命周期监听 - WidgetsBinding.instance.addObserver(this); - // 初始化所有Tab - _tabVisibility = [true, true, true]; + // 初始化固定页面(顺序固定) — **这里保持你要求的构造不变** _pages = [ HomePage(key: _homeKey, isChooseFirm: widget.isChooseFirm), NotifPage(key: _notifKey), MinePage(key: _mineKey, isChooseFirm: widget.isChooseFirm), ]; - // 启动心跳服务 - HeartbeatService().start(); + + // 初始化时默认隐藏,等待路由加载后再决定显示哪些 tab + _tabVisibility = [false, false, false]; + + // 监听 RouteService 的更新:当路由数据加载/变更时更新可见性 + RouteService().addListener(_onRoutesUpdated); + + // 拉取路由(优先接口,失败则回退本地 assets) + _getRoute(); + } + + /// 拉取路由:优先通过接口获取;如果接口失败或返回异常,回退到 assets/route/routes.txt + Future _getRoute() async { + try { + LoadingDialogHelper.show(message: '加载中...'); + + Map? route; + // 接口获取 + // try { + // final res = await AppMenuApi.getAppMenu(); + // if (res != null && res['success'] == true && res['data'] is List) { + // route = res; + // } else { + // debugPrint('AppMenuApi.getAppMenu returned no data or failed; fallback to local assets.'); + // } + // } catch (e) { + // debugPrint('AppMenuApi.getAppMenu error: $e -> fallback to local assets.'); + // } + // 本地获取 + try { + final routeString = await loadFromAssets(); + route = jsonDecode(routeString) as Map; + } catch (e) { + debugPrint('loadFromAssets error: $e'); + } + + if (route != null && route['data'] is List) { + final data = route['data'] as List; + RouteService().initializeRoutes(data); + // initializeRoutes 内部会 notifyListeners -> _onRoutesUpdated 被调用 + } else { + debugPrint('No valid route data to initialize RouteService.'); + } + } catch (e) { + debugPrint('获取路由配置失败: $e'); + } finally { + LoadingDialogHelper.hide(); + } + } + + Future loadFromAssets() async { + return await rootBundle.loadString('assets/route/routes.txt'); + } + + void _onRoutesUpdated() { + // 路由服务更新时重新计算三个模块的可见性 + _updateTabVisibilityFromRoutes(); + } + + /// 根据 RouteService 中解析的路由来设置 _tabVisibility + /// 规则: + /// - 首页:menuPerms == 'dashboard' + /// - 通知:menuPerms == 'notice' + /// - 我的:menuPerms == 'my-center' + void _updateTabVisibilityFromRoutes() { + final routeService = RouteService(); + + // 使用 mainTabs,如果为空(尚未有路由或后端返回空),保持当前 _tabVisibility(即不自动显示) + final mainTabs = routeService.mainTabs; + if (mainTabs.isEmpty) { + return; + } + + bool homeVisible = false; + bool notifVisible = false; + bool mineVisible = false; + + for (final m in mainTabs) { + final perms = (m.menuPerms ?? '').toString(); + if (!homeVisible && perms == 'dashboard' && m.visible) { + homeVisible = true; + } + if (!notifVisible && perms == 'notice'&& m.visible) { + notifVisible = true; + } + if (!mineVisible && perms == 'my-center'&& m.visible) { + mineVisible = true; + } + if (homeVisible && notifVisible && mineVisible) break; + } + + // 后端未匹配到就隐藏(不兜底) + setState(() { + _tabVisibility = [homeVisible, notifVisible, mineVisible]; + + // 若当前激活的 tab 被隐藏,则切换到第一个可见 tab(若没有可见 tab,则保持当前索引为 0) + if (!_isIndexVisible(_currentIndex)) { + final first = _firstVisibleIndexOrDefault(_currentIndex); + _currentIndex = first; + } + }); + } + + // 返回第一个可见 tab 的索引;如果没有可见项,返回给定的 fallback(默认0) + int _firstVisibleIndexOrDefault([int fallback = 0]) { + for (int i = 0; i < _tabVisibility.length; i++) { + if (_tabVisibility[i]) return i; + } + return fallback; + } + + bool _isIndexVisible(int index) { + if (index < 0 || index >= _tabVisibility.length) return false; + return _tabVisibility[index]; } @override void dispose() { + // 移除监听 _badgeManager.removeListener(_onBadgeChanged); - // 移除生命周期监听 + RouteService().removeListener(_onRoutesUpdated); WidgetsBinding.instance.removeObserver(this); - - // 停止心跳服务 HeartbeatService().stop(); - super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - switch (state) { case AppLifecycleState.resumed: - // 应用回到前台,恢复心跳 HeartbeatService().resume(); break; case AppLifecycleState.paused: case AppLifecycleState.inactive: case AppLifecycleState.detached: case AppLifecycleState.hidden: - // 应用进入后台,暂停心跳 HeartbeatService().pause(); break; } @@ -134,33 +248,53 @@ class _MainPageState extends State with WidgetsBindingObserver { ); } + // 将原始索引(固定顺序 0..n) 转为 可见索引 (visiblePages 索引) + // 若 originalIndex 在当前不可见,则返回第一个可见的索引(若无可见则返回 0) + int _originalToVisibleIndex(int originalIndex, List visibility) { + int visibleIndex = 0; + for (int i = 0; i < visibility.length; i++) { + if (!visibility[i]) continue; + if (i == originalIndex) return visibleIndex; + visibleIndex++; + } + // originalIndex 不可见 -> 返回第一个可见索引(如果没有可见返回 0) + return visibility.contains(true) ? 0 : 0; + } + + // 将可见索引映射回原始索引(用于 BottomNavigationBar 的 onTap) + // 若 visibleIndex 超界,返回第一个可见原始索引或 0 + int _visibleToOriginalIndex(int visibleIndex, List visibility) { + int count = 0; + for (int i = 0; i < visibility.length; i++) { + if (!visibility[i]) continue; + if (count == visibleIndex) return i; + count++; + } + // 若没有找到,返回第一个可见原始索引或 0 + for (int i = 0; i < visibility.length; i++) { + if (visibility[i]) return i; + } + return 0; + } + @override Widget build(BuildContext context) { + // 使用 _badgeManager 而不是 BadgeManager() final bm = _badgeManager; - // 构建可见的底部导航项 + // 构建可见的底部导航项与页面(基于 _tabVisibility) final List visibleItems = []; final List visiblePages = []; - for (int i = 0; i < _tabVisibility.length; i++) { + for (int i = 0; i < _pages.length; i++) { if (_tabVisibility[i]) { switch (i) { case 0: - visibleItems.add( - BottomNavigationBarItem( - icon: Image.asset( - 'assets/tabbar/basics.png', - width: 24, - height: 24, - ), - activeIcon: Image.asset( - 'assets/tabbar/basics_cur.png', - width: 24, - height: 24, - ), - label: '首页', - ), - ); + visibleItems.add(BottomNavigationBarItem( + icon: Image.asset('assets/tabbar/basics.png', width: 24, height: 24), + activeIcon: Image.asset('assets/tabbar/basics_cur.png', width: 24, height: 24), + label: '首页', + )); visiblePages.add(_pages[i]); break; case 1: @@ -178,83 +312,105 @@ class _MainPageState extends State with WidgetsBindingObserver { visiblePages.add(_pages[i]); break; case 2: - visibleItems.add( - BottomNavigationBarItem( - icon: Image.asset( - 'assets/tabbar/my.png', - width: 24, - height: 24, - ), - activeIcon: Image.asset( - 'assets/tabbar/my_cur.png', - width: 24, - height: 24, - ), - label: '我的', - ), - ); + visibleItems.add(BottomNavigationBarItem( + icon: Image.asset('assets/tabbar/my.png', width: 24, height: 24), + activeIcon: Image.asset('assets/tabbar/my_cur.png', width: 24, height: 24), + label: '我的', + )); visiblePages.add(_pages[i]); break; } } } - // 将当前索引映射到可见Tab的索引 - int getVisibleIndex(int originalIndex) { - int visibleIndex = 0; - for (int i = 0; i <= originalIndex; i++) { - if (_tabVisibility[i]) { - if (i == originalIndex) return visibleIndex; - visibleIndex++; - } - } - return 0; // 默认返回第一个可见Tab - } + // 如果没有任何可见页面,body 显示空占位(按你之前要求不兜底) + final bool hasVisiblePages = visiblePages.isNotEmpty; - final visibleCurrentIndex = getVisibleIndex(_currentIndex); + // 将当前索引映射到可见Tab的索引(用于 IndexedStack/BottomNavigationBar) + final visibleCurrentIndex = _originalToVisibleIndex(_currentIndex, _tabVisibility); + + // ---------- 关键:根据 visibleItems 个数选择底栏渲染方式 ---------- + Widget? bottomBarWidget; + if (visibleItems.length >= 2) { + // 正常使用 BottomNavigationBar(至少 2 个 item) + bottomBarWidget = BottomNavigationBar( + currentIndex: visibleCurrentIndex, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.grey, + onTap: (visibleIndex) { + final originalIndex = _visibleToOriginalIndex(visibleIndex, _tabVisibility); + setState(() => _currentIndex = originalIndex); + }, + items: visibleItems, + ); + } else if (visibleItems.length == 1) { + // 自定义单个 tab 底栏(避免 BottomNavigationBar 的断言) + final single = visibleItems[0]; + final singleVisibleOriginalIndex = _visibleToOriginalIndex(0, _tabVisibility); + final isSelected = _currentIndex == singleVisibleOriginalIndex; + + // 取选中或未选中的 icon 小部件 + final Widget iconWidget = isSelected && single.activeIcon != null ? single.activeIcon! : single.icon; + + bottomBarWidget = Material( + elevation: 8, + child: InkWell( + onTap: () { + setState(() { + _currentIndex = singleVisibleOriginalIndex; + }); + }, + child: Container( + height: kBottomNavigationBarHeight, + color: Colors.white, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 图标(上) + SizedBox( + width: 28, + height: 28, + child: Center(child: iconWidget), + ), + const SizedBox(height: 4), + // 文本(下) + Text( + single.label ?? '', + style: TextStyle( + color: isSelected ? Colors.blue : Colors.grey, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ), + ); + } else { + // 没有可见项 -> 不显示底栏 + bottomBarWidget = null; + } return CurrentTabNotifier( currentIndex: _currentIndex, child: Scaffold( appBar: null, - body: IndexedStack(index: visibleCurrentIndex, children: visiblePages), - bottomNavigationBar: - visibleItems.length >= 2 - ? BottomNavigationBar( - currentIndex: visibleCurrentIndex, - type: BottomNavigationBarType.fixed, - selectedItemColor: Colors.blue, - unselectedItemColor: Colors.grey, - onTap: (visibleIndex) { - int originalIndex = 0; - int count = 0; - for (int i = 0; i < _tabVisibility.length; i++) { - if (_tabVisibility[i]) { - if (count == visibleIndex) { - originalIndex = i; - break; - } - count++; - } - } - setState(() => _currentIndex = originalIndex); - }, - items: visibleItems, - ) - : null, // 如果没有可见的Tab,隐藏底部导航栏 + body: hasVisiblePages + ? IndexedStack( + index: visibleCurrentIndex, + children: visiblePages, + ) + : const SizedBox.shrink(), + bottomNavigationBar: bottomBarWidget, ), ); } - void _onBadgeChanged() { // 当角标数据变化时,只更新需要重建的部分 - // 但这里我们只需要触发 build 来更新底部导航栏的角标 - if (mounted) { - setState(() { - // 只触发重建,不改变数据 - }); - } + if (mounted) setState(() {}); } - -} +} \ No newline at end of file diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart index bb21103..163307d 100644 --- a/lib/pages/mine/mine_page.dart +++ b/lib/pages/mine/mine_page.dart @@ -1,5 +1,7 @@ +// file: mine_page.dart import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; @@ -19,10 +21,13 @@ import 'package:qhd_prevention/tools/tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'certificate/certificate_list_page.dart'; +import 'package:qhd_prevention/common/route_service.dart'; +import 'package:qhd_prevention/common/route_model.dart'; class MinePage extends StatefulWidget { const MinePage({super.key, required this.isChooseFirm}); -final bool isChooseFirm; + final bool isChooseFirm; + @override State createState() => MinePageState(); } @@ -39,22 +44,152 @@ class MinePageState extends State { String name = '登录/注册'; String phone = ''; + /// 配置:UI 顺序/图标/对应后端 menuPerm(s) + /// perm 列表为空表示不受后端配置控制(始终显示) + final List> _allSettings = [ + { + 'title': '我的信息', + 'icon': 'assets/images/ico9.png', + 'perms': ['my-center-My-Information'], + 'action': 'userinfo', + }, + { + 'title': '修改密码', + 'icon': 'assets/images/ico16.png', + 'perms': ['my-center-Change-Password'], + 'action': 'changePwd', + }, + { + 'title': '扫码入职', + 'icon': 'assets/images/ico10.png', + // 如果你希望扫码入职也受后端控制,可以保留下面 perm;否则置空 [] + 'perms': ['dashboard-scan'], + 'action': 'scanOnboarding', + }, + { + 'title': '人脸认证', + 'icon': 'assets/images/ico11.png', + 'perms': ['my-center-Face-Authentication'], + 'action': 'face', + }, + { + 'title': '证书信息', + 'icon': 'assets/images/ico12.png', + 'perms': ['my-center-Certificate-Information'], + 'action': 'certificate', + }, + // { + // 'title': '版本更新', + // 'icon': 'assets/images/ico14.png', + // 'perms': ['my-center-Version-Update'], + // 'action': 'version', + // }, + // { + // 'title': '关于我们', + // 'icon': 'assets/images/ico15.png', + // 'perms': ['my-center-About-Us'], + // 'action': 'about', + // }, + { + 'title': '切换账户', + 'icon': 'assets/images/ico_switch.png', + // 切换账户仍只在 widget.isChooseFirm 为 true 时显示(参见构建函数) + 'perms': ['my-center-Switch-Account'], + 'action': 'changeAccount', + }, + { + 'title': '账户注销', + 'icon': 'assets/images/ico_quit.png', + // 后端可能有多种命名,两者任一存在即显示 + 'perms': ['my-center-User-Logout', 'my-center-Logout'], + 'action': 'logout', + }, + ]; + + // 当前可见项(与 _allSettings 顺序对应) + late List _visible; + void onRouteConfigLoaded() { if (mounted) { setState(() { - // _updateMenuVisibility(); + // 这里不直接修改业务逻辑,仅触发重建(update 会在 listener 中执行) }); } } @override void initState() { - // TODO: implement initState super.initState(); _getUserInfo(); + + // 初始默认都显示,避免短时白屏/闪烁 + _visible = List.filled(_allSettings.length, true); + + // 监听路由变化来决定哪些设置项应显示 + RouteService().addListener(_onRouteServiceUpdated); + + // 尝试立即应用(如果 route 已加载) + WidgetsBinding.instance.addPostFrameCallback((_) => _updateVisibilityFromRoute()); } - // 获取用户信息 + + @override + void dispose() { + try { + RouteService().removeListener(_onRouteServiceUpdated); + } catch (_) {} + super.dispose(); + } + + void _onRouteServiceUpdated() { + _updateVisibilityFromRoute(); + } + + /// 根据 RouteService 的路由配置计算每个条目的可见性 + void _updateVisibilityFromRoute() { + final rs = RouteService(); + + // 如果 mainTabs 为空,说明路由还没加载;保持当前 visible(避免闪烁) + if (rs.mainTabs.isEmpty) { + return; + } + + final List next = List.filled(_allSettings.length, false); + + for (int i = 0; i < _allSettings.length; i++) { + final perms = (_allSettings[i]['perms'] ?? []) as List; + + if (perms.isEmpty) { + // 无后端控制的项默认显示(比如你希望用户协议/隐私总是能见到) + next[i] = true; + continue; + } + + // 只要找到任一 perm 在路由树中可见,即认为该项可见 + bool anyFound = false; + for (final p in perms) { + final node = rs.findRouteByPerm(p); + if (node != null) { + // rs.findRouteByPerm 已经会考虑父节点 visible 的情况(按照 RouteService 实现) + // 这里再用 showFlag==1 做额外判断:当后端返回节点但 showFlag==0 (不显示) 则视为不可见 + if (node.showFlag == 1 || node.visible) { + anyFound = true; + break; + } + } + } + next[i] = anyFound; + } + + // 与当前 _visible 比较,只有变更时才 setState + if (!listEquals(next, _visible)) { + setState(() { + _visible = next; + }); + } + } + + // 获取用户信息(保持原逻辑) Future _getUserInfo() async { final res = await BasicInfoApi.getUserMessage( '${SessionService.instance.accountId}', @@ -69,111 +204,8 @@ class MinePageState extends State { }); } } - @override - Widget build(BuildContext context) { - final double headerHeight = 300.0; - final double overlap = 100.0; - return SizedBox( - height: MediaQuery.of(context).size.height, - child: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - height: headerHeight, - child: _buildHeaderSection(), - ), - Positioned.fill( - child: NotificationListener( - onNotification: (overscroll) { - overscroll.disallowIndicator(); - return false; - }, - child: ListView( - padding: EdgeInsets.only( - top: headerHeight - overlap, - bottom: 24, - left: 0, - right: 0, - ), - children: [ - _buildSettingsList(), - - SizedBox(height: 15), - - Padding( - padding: EdgeInsetsGeometry.symmetric(horizontal: 15), - child: CustomButton( - text: '退出登录', - textStyle: TextStyle(fontSize: 16), - backgroundColor: Colors.blue, - // borderRadius: 15, - onPressed: () { - CustomAlertDialog.showConfirm( - context, - title: '确认退出', - content: '确定要退出当前账号吗', - onConfirm: () async { - // await AuthService.logout(); // ✅ 等待登出完成 - // if (!mounted) return; - // 清除用户登录状态 - await _clearUserSession(); - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => const LoginPage(), - ), - (Route route) => false, - ); - }, - ); - }, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - - Widget _buildHeaderSection() { - return Stack( - alignment: const FractionalOffset(0.5, 0), - children: [ - Padding( - padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: - Image.asset( - "assets/images/my_bg.png", - width: MediaQuery.of(context).size.width, // 获取屏幕宽度 - fit: BoxFit.cover, - ), - ), - - Positioned( - top: 51, - child: Text( - "我的", - style: TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - - // 标语区域 - _buildSloganSection(), - ], - ); - } Future _logout() async { - LoadingDialogHelper.show(); /// 获取用户在职列表 @@ -192,8 +224,7 @@ class MinePageState extends State { context, title: '温馨提示', content: '注销后您的所有信息将会被删除\n请确认是否注销。 ', - onConfirm: () { - }, + onConfirm: () {}, ); if (result) { CustomAlertDialog.showInputWithCode( @@ -209,8 +240,7 @@ class MinePageState extends State { }, onConfirm: (code) async { _quit(code); - } - ); + }); } } } else { @@ -223,8 +253,8 @@ class MinePageState extends State { Future _quit(String code) async { LoadingDialogHelper.show(); Map data = { - 'id' : SessionService.instance.accountId, - 'phoneCode' : code, + 'id': SessionService.instance.accountId, + 'phoneCode': code, }; await BasicInfoApi.logout(data).then((res) async { LoadingDialogHelper.dismiss(); @@ -233,8 +263,7 @@ class MinePageState extends State { await SessionService.instance.clear(clearPrefs: true); Navigator.pushReplacement( context, - MaterialPageRoute( - builder: (_) => const LoginPage()), + MaterialPageRoute(builder: (_) => const LoginPage()), ); } else { ToastUtil.showNormal(context, res['errMessage'] ?? ''); @@ -246,28 +275,21 @@ class MinePageState extends State { final headerUrl = SessionService.instance.userData?.userAvatarUrl ?? ''; return Container( - margin: EdgeInsets.fromLTRB(0, 100, 0, 0), + margin: const EdgeInsets.fromLTRB(0, 100, 0, 0), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, // 水平居中 - // crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中(可选) + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - headerUrl.isEmpty - ? const CircleAvatar( - backgroundImage: AssetImage("assets/images/my_bg.png"), - radius: 30, - ) - : CircleAvatar( - backgroundImage: NetworkImage(ApiService.baseImgPath + headerUrl), - radius: 30, - ), - + const CircleAvatar( + backgroundImage: AssetImage("assets/images/yingyong11.png"), + radius: 30, + ), const SizedBox(width: 16), Text( name, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.white, @@ -281,136 +303,6 @@ class MinePageState extends State { ); } - Widget _buildSettingsList() { - return Container( - margin: const EdgeInsets.fromLTRB(20, 0, 20, 0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - spreadRadius: 2, - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - _buildSettingItem( - title: "我的信息", - icon: "assets/images/ico9.png", - value: notificationsEnabled, - onChanged: (value) async { - await pushPage( - FullUserinfoPage(isEidt: false, isChooseFirm: true), - context, - ); - - }, - ), - - _buildSettingItem( - title: "修改密码", - icon: "assets/images/ico16.png", - value: notificationsEnabled, - onChanged: (value) async { - await pushPage(MineSetPwdPage('0'), context); - }, - ), - _buildSettingItem( - title: "扫码入职", - icon: "assets/images/ico10.png", - value: scanAuthentication, - onChanged: (value) async { - final result = await pushPage( - ScanPage(type: ScanType.Onboarding), - context, - ); - if (result == null) { - return; - } - pushPage(OnboardingFullPage(scanData: result), context); - }, - ), - - _buildSettingItem( - title: "人脸认证", - icon: "assets/images/ico11.png", - value: faceAuthentication, - onChanged: (value) { - pushPage( - const FaceRecognitionPage( - studentId: '', - data: {}, - mode: FaceMode.setUpdata, - ), - context, - ); - }, - ), - - _buildSettingItem( - title: "证书信息", - icon: "assets/images/ico12.png", - value: passwordChanged, - onChanged: (value) { - pushPage( - const CertificateListPage(), - context, - ); - }, - ), - - // _buildSettingItem( - // title: "问题反馈", - // icon: "assets/images/ico13.png", - // value: passwordChanged, - // onChanged: (value) { - // ToastUtil.showNormal(context, '需求待定'); - // // pushPage(FeedbackPage(), context); - // }, - // ), - - // const Divider(height: 1, indent: 60), - _buildSettingItem( - title: "版本更新", - icon: "assets/images/ico14.png", - value: updateAvailable, - onChanged: (value) => setState(() => updateAvailable = value!), - ), - - _buildSettingItem( - title: "关于我们", - icon: "assets/images/ico15.png", - value: logoutSelected, - onChanged: (value) { - setState(() => logoutSelected = value!); - }, - ), - if (widget.isChooseFirm) - _buildSettingItem( - title: "切换账户", - icon: "assets/images/ico_switch.png", - value: logoutSelected, - onChanged: (value) { - pushPage(MineChangeFirmPage(), context); - }, - ), - _buildSettingItem( - title: "账户注销", - icon: "assets/images/ico_quit.png", - value: logoutSelected, - onChanged: (value) { - _logout(); - }, - ), - ], - ), - ); - } - Widget _buildSettingItem({ required String title, required String icon, @@ -435,16 +327,9 @@ class MinePageState extends State { title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), ), - trailing: Transform.scale( scale: 1.2, - child: Icon(Icons.chevron_right), - // Image.asset( - // "assets/images/right.png", - // fit: BoxFit.cover, - // width: 15, - // height: 15, - // ), + child: const Icon(Icons.chevron_right), ), ), ); @@ -454,4 +339,160 @@ class MinePageState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.remove('isLoggedIn'); // 清除登录状态 } -} + + void _onSettingTapAction(String action) async { + switch (action) { + case 'userinfo': + await pushPage(FullUserinfoPage(isEidt: false, isChooseFirm: true), context); + break; + case 'changePwd': + await pushPage(MineSetPwdPage('0'), context); + break; + case 'scanOnboarding': + final result = await pushPage(ScanPage(type: ScanType.Onboarding), context); + if (result == null) return; + pushPage(OnboardingFullPage(scanData: result), context); + break; + case 'face': + pushPage(const FaceRecognitionPage(studentId: '', data: {}, mode: FaceMode.setUpdata), context); + break; + case 'certificate': + pushPage(const CertificateListPage(), context); + break; + case 'version': + // 版本检查逻辑(占位) + ToastUtil.showNormal(context, '功能开发中...'); + break; + case 'about': + // 关于我们(占位) + ToastUtil.showNormal(context, '功能开发中...'); + + break; + case 'changeAccount': + pushPage(MineChangeFirmPage(), context); + break; + case 'logout': + _logout(); + break; + default: + break; + } + } + + Widget _buildSettingsList() { + final children = []; + for (int i = 0; i < _allSettings.length; i++) { + // 保证索引范围并且按照 _visible 判定 + if (i >= _visible.length) continue; + if (!_visible[i]) continue; + + final item = _allSettings[i]; + final title = item['title'] as String; + + // 保持原来逻辑:'切换账户' 只在 isChooseFirm 为 true 时显示 + if (title == '切换账户' && !widget.isChooseFirm) continue; + + children.add(_buildSettingItem( + title: title, + icon: item['icon'] as String, + value: false, + onChanged: (_) => _onSettingTapAction(item['action'] as String), + )); + } + + return Container( + margin: const EdgeInsets.fromLTRB(20, 0, 20, 0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: children, + ), + ); + } + + @override + Widget build(BuildContext context) { + final double headerHeight = 300.0; + final double overlap = 100.0; + return SizedBox( + height: MediaQuery.of(context).size.height, + child: Stack( + children: [ + Positioned(top: 0, left: 0, right: 0, height: headerHeight, child: _buildHeaderSection()), + Positioned.fill( + child: NotificationListener( + onNotification: (overscroll) { + overscroll.disallowIndicator(); + return false; + }, + child: ListView( + padding: EdgeInsets.only(top: headerHeight - overlap, bottom: 24, left: 0, right: 0), + children: [ + _buildSettingsList(), + const SizedBox(height: 15), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: CustomButton( + text: '退出登录', + textStyle: const TextStyle(fontSize: 16), + backgroundColor: Colors.blue, + onPressed: () { + CustomAlertDialog.showConfirm( + context, + title: '确认退出', + content: '确定要退出当前账号吗', + onConfirm: () async { + await _clearUserSession(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), + (Route route) => false, + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildHeaderSection() { + return Stack( + alignment: const FractionalOffset(0.5, 0), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 10), + child: Image.asset( + "assets/images/my_bg.png", + width: MediaQuery.of(context).size.width, + fit: BoxFit.cover, + ), + ), + const Positioned( + top: 51, + child: Text( + "我的", + style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + _buildSloganSection(), + ], + ); + } +} \ No newline at end of file diff --git a/lib/services/scan_service.dart b/lib/services/scan_service.dart new file mode 100644 index 0000000..13e87db --- /dev/null +++ b/lib/services/scan_service.dart @@ -0,0 +1,131 @@ + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/constants/app_enums.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/http/modules/edu_api.dart'; +import 'package:qhd_prevention/http/modules/file_api.dart'; +import 'package:qhd_prevention/pages/home/Study/study_take_exam_page.dart'; +import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; +import 'package:qhd_prevention/pages/mine/mine_sign_page.dart'; +import 'package:qhd_prevention/pages/mine/onboarding_full_page.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class ScanService { + static void scan(BuildContext context, final result) async { + if (FormUtils.hasValue(result, 'classId')) { // 线上培训相关 + int type = result['type'] ?? 0; + + final data = { + ...result, + 'phone': SessionService.instance.userData?.phone ?? '', + 'type': type, + }; + LoadingDialogHelper.show(); + + // 验证是否可以签到 + final response = await EduApi.checkSignIn(data); + LoadingDialogHelper.hide(); + + if (response['success']) { + // 进行人脸识别 + final filePath = await pushPage( + const FaceRecognitionPage( + studentId: '', + data: {}, + mode: FaceMode.study, + ), + context, + ); + + final faceData = response['data']; + + if (filePath != null) { + // 对比人脸 + try { + LoadingDialogHelper.show(); + final response = await EduApi.compareFace({ + 'type': data['type'], + 'studentId': faceData['studentId'], + }, filePath); + final faceResultData = response['data']; + if (response['success']) { + final signData = { + 'id': faceResultData['id'] ?? '', + 'studentId': faceResultData['studentId'] ?? '', + 'type': data['type']?? '', + 'classId': faceResultData['classId'] ?? '', + 'studentSignId':faceResultData['studentSignId']??'' + }; + ScanService.signUpload(signData, type, context); + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, response['errMessage'] ?? '验证失败'); + } + } catch (e) { + ToastUtil.showNormal(context,'验证失败'); + LoadingDialogHelper.hide(); + print(e); + } + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '签到失败'); + } + } else { + ToastUtil.showNormal(context, response['errMessage'] ?? '签到失败'); + } + } + if (FormUtils.hasValue(result, 'corpinfoId')) { // 入职 + pushPage(OnboardingFullPage(scanData: result), context); + } + + } + // 上传签到签字 + static Future signUpload(Map data, int type, BuildContext context) async { + LoadingDialogHelper.hide(); + UploadFileType fileType = + type == 1 + ? UploadFileType.onlineLearningSignSignature + : UploadFileType.onlineLearningExamSignature; + final signPath = await pushPage(MineSignPage(), context); + if (signPath != null) { + // 先上传签字图片 + try { + LoadingDialogHelper.show(); + // 上传图片 + final response = await FileApi.uploadFile(signPath, fileType, ''); + if (response['success']) { + data['signUrl'] = response['data']['filePath']; + final signResult = await EduApi.uploadSignature(data); + LoadingDialogHelper.hide(); + if (signResult['success']) { + if (type == 1) { + ToastUtil.showNormal(context, '签到成功'); + } else { + // 获取试卷详情 + final examResult = await EduApi.getExamDetail( + data['classId'] ?? '', + ); + LoadingDialogHelper.hide(); + // 跳转考试页面 + pushPage( + StudyTakeExamPage( + examInfo: examResult['data'] ?? {}, signInfo: data), + context, + ); + } + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, signResult['errMessage'] ?? ''); + } + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, response['errMessage'] ?? ''); + } + } catch (e) { + LoadingDialogHelper.hide(); + print(e); + } + } + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index d032ffb..85afea2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -155,6 +155,7 @@ flutter: - assets/map/ - assets/tabbar/ - assets/study/ + - assets/route/routes.txt # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg