diff --git a/assets/images/banner.png b/assets/images/banner.png index b45ab94..bcba775 100644 Binary files a/assets/images/banner.png and b/assets/images/banner.png differ diff --git a/assets/images/home_banner.png b/assets/images/home_banner.png index ab6fe67..ec334b2 100644 Binary files a/assets/images/home_banner.png and b/assets/images/home_banner.png differ diff --git a/assets/images/ico_quit.png b/assets/images/ico_quit.png new file mode 100644 index 0000000..cde6c12 Binary files /dev/null and b/assets/images/ico_quit.png differ diff --git a/assets/images/ico_switch.png b/assets/images/ico_switch.png new file mode 100644 index 0000000..6caddaa Binary files /dev/null and b/assets/images/ico_switch.png differ diff --git a/assets/images/icon_study01.png b/assets/images/icon_study01.png new file mode 100644 index 0000000..0404c11 Binary files /dev/null and b/assets/images/icon_study01.png differ diff --git a/assets/images/login_top_bg.png b/assets/images/login_top_bg.png index fafc1db..d35886b 100644 Binary files a/assets/images/login_top_bg.png and b/assets/images/login_top_bg.png differ diff --git a/assets/images/login_top_shader.png b/assets/images/login_top_shader.png index 511ccb6..5bc363c 100644 Binary files a/assets/images/login_top_shader.png and b/assets/images/login_top_shader.png differ diff --git a/assets/images/loginbg.png b/assets/images/loginbg.png index d2ab811..960773d 100644 Binary files a/assets/images/loginbg.png and b/assets/images/loginbg.png differ diff --git a/assets/images/null.png b/assets/images/null.png index 7df785c..052deae 100644 Binary files a/assets/images/null.png and b/assets/images/null.png differ diff --git a/assets/images/study_banner.png b/assets/images/study_banner.png new file mode 100644 index 0000000..3e0381e Binary files /dev/null and b/assets/images/study_banner.png differ diff --git a/assets/images/unit_banner.jpg b/assets/images/unit_banner.jpg index bb3daae..311fb6b 100644 Binary files a/assets/images/unit_banner.jpg and b/assets/images/unit_banner.jpg differ diff --git a/assets/study/bgimg1.png b/assets/study/bgimg1.png new file mode 100644 index 0000000..004c26a Binary files /dev/null and b/assets/study/bgimg1.png differ diff --git a/assets/study/copy-one.png b/assets/study/copy-one.png new file mode 100644 index 0000000..e8b05db Binary files /dev/null and b/assets/study/copy-one.png differ diff --git a/assets/study/err.png b/assets/study/err.png new file mode 100644 index 0000000..ef99366 Binary files /dev/null and b/assets/study/err.png differ diff --git a/assets/study/play.png b/assets/study/play.png new file mode 100644 index 0000000..ea6e626 Binary files /dev/null and b/assets/study/play.png differ diff --git a/assets/study/right.png b/assets/study/right.png new file mode 100644 index 0000000..0fa67d7 Binary files /dev/null and b/assets/study/right.png differ diff --git a/assets/study/time.png b/assets/study/time.png new file mode 100644 index 0000000..9510c6b Binary files /dev/null and b/assets/study/time.png differ diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5b28235..00d8e94 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -70,8 +70,6 @@ PODS: - mobile_scanner (7.0.0): - Flutter - FlutterMacOS - - nfc_manager (0.0.1): - - Flutter - objective_c (0.0.1): - Flutter - open_file_ios (0.0.1): @@ -120,7 +118,6 @@ DEPENDENCIES: - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - - nfc_manager (from `.symlinks/plugins/nfc_manager/ios`) - objective_c (from `.symlinks/plugins/objective_c/ios`) - open_file_ios (from `.symlinks/plugins/open_file_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -171,8 +168,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/image_picker_ios/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/darwin" - nfc_manager: - :path: ".symlinks/plugins/nfc_manager/ios" objective_c: :path: ".symlinks/plugins/objective_c/ios" open_file_ios: @@ -216,7 +211,6 @@ SPEC CHECKSUMS: geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 - nfc_manager: f6d5609c09b4640b914a3dc67479a2e392965fd0 objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 open_file_ios: 5ff7526df64e4394b4fe207636b67a95e83078bb package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c7fc0b7..53c7fc4 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -491,13 +491,14 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -507,7 +508,7 @@ PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq-des"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -686,13 +687,14 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -702,7 +704,7 @@ PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq-des"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -718,13 +720,14 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 8AKCJ9LW7D; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "秦港安全"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -734,7 +737,7 @@ PRODUCT_BUNDLE_IDENTIFIER = uni.UNI85F7A17; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq-des"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/lib/constants/app_enums.dart b/lib/constants/app_enums.dart index 3f0cab2..d0fbf09 100644 --- a/lib/constants/app_enums.dart +++ b/lib/constants/app_enums.dart @@ -33,7 +33,10 @@ enum UploadFileType { hiddenDangerRectificationPlan('8', 'hidden_danger_rectification_plan'), /// 有限空间确认人签字 - 类型: '9', 路径: 'confined_space_confirmation_signature' - confinedSpaceConfirmationSignature('9', 'confined_space_confirmation_signature'), + confinedSpaceConfirmationSignature( + '9', + 'confined_space_confirmation_signature', + ), /// 劳动合同图片 - 类型: '10', 路径: 'labor_contract_image' laborContractImage('10', 'labor_contract_image'), @@ -54,7 +57,10 @@ enum UploadFileType { socialSecurityCardPhoto('15', 'social_security_card_photo'), /// 工伤保险凭证 - 类型: '16', 路径: 'work_related_injury_insurance_certificate' - workRelatedInjuryInsuranceCertificate('16', 'work_related_injury_insurance_certificate'), + workRelatedInjuryInsuranceCertificate( + '16', + 'work_related_injury_insurance_certificate', + ), /// 特种设备巡检照片 - 类型: '17', 路径: 'special_equipment_inspection_photo' specialEquipmentInspectionPhoto('17', 'special_equipment_inspection_photo'), @@ -63,25 +69,46 @@ enum UploadFileType { personnelCertificate('18', 'personnel_certificate'), /// 三级教育培训 - 类型: '19', 路径: 'three_level_safety_education_training' - threeLevelSafetyEducationTraining('19', 'three_level_safety_education_training'), + threeLevelSafetyEducationTraining( + '19', + 'three_level_safety_education_training', + ), /// 重大危险源报警处置前照片 - 类型: '20', 路径: 'major_hazard_source_alarm_before_handling_photo' - majorHazardSourceAlarmBeforeHandlingPhoto('20', 'major_hazard_source_alarm_before_handling_photo'), + majorHazardSourceAlarmBeforeHandlingPhoto( + '20', + 'major_hazard_source_alarm_before_handling_photo', + ), /// 重大危险源报警处置后照片 - 类型: '21', 路径: 'major_hazard_source_alarm_after_handling_photo' - majorHazardSourceAlarmAfterHandlingPhoto('21', 'major_hazard_source_alarm_after_handling_photo'), + majorHazardSourceAlarmAfterHandlingPhoto( + '21', + 'major_hazard_source_alarm_after_handling_photo', + ), /// 智能门禁外来车辆驾驶证照片 - 类型: '22', 路径: 'smart_access_control_external_vehicle_driver_license_photo' - smartAccessControlExternalVehicleDriverLicensePhoto('22', 'smart_access_control_external_vehicle_driver_license_photo'), + smartAccessControlExternalVehicleDriverLicensePhoto( + '22', + 'smart_access_control_external_vehicle_driver_license_photo', + ), /// 智能门禁外来车辆行驶证照片 - 类型: '23', 路径: 'smart_access_control_external_vehicle_registration_photo' - smartAccessControlExternalVehicleRegistrationPhoto('23', 'smart_access_control_external_vehicle_registration_photo'), + smartAccessControlExternalVehicleRegistrationPhoto( + '23', + 'smart_access_control_external_vehicle_registration_photo', + ), /// 安全环保检查终验图片 - 类型: '50', 路径: 'safety_and_environmental_inspection_final_acceptance_image' - safetyAndEnvironmentalInspectionFinalAcceptanceImage('50', 'safety_and_environmental_inspection_final_acceptance_image'), + safetyAndEnvironmentalInspectionFinalAcceptanceImage( + '50', + 'safety_and_environmental_inspection_final_acceptance_image', + ), /// 隐患延期临时措施附件 - 类型: '101', 路径: 'hidden_danger_extension_temporary_measures_attachment' - hiddenDangerExtensionTemporaryMeasuresAttachment('101', 'hidden_danger_extension_temporary_measures_attachment'), + hiddenDangerExtensionTemporaryMeasuresAttachment( + '101', + 'hidden_danger_extension_temporary_measures_attachment', + ), /// 隐患视频 - 类型: '102', 路径: 'hidden_danger_video' hiddenDangerVideo('102', 'hidden_danger_video'), @@ -96,28 +123,49 @@ enum UploadFileType { rectificationSuggestionsAndPlan('107', 'rectification_suggestions_and_plan'), /// 重大隐患上传隐患调查报告 - 类型: '108', 路径: 'major_hidden_danger_investigation_report' - majorHiddenDangerInvestigationReport('108', 'major_hidden_danger_investigation_report'), + majorHiddenDangerInvestigationReport( + '108', + 'major_hidden_danger_investigation_report', + ), /// 重大隐患安委会或党委会决议记录 - 类型: '109', 路径: 'major_hidden_danger_safety_committee_or_party_committee_resolution_record' - majorHiddenDangerSafetyCommitteeOrPartyCommitteeResolutionRecord('109', 'major_hidden_danger_safety_committee_or_party_committee_resolution_record'), + majorHiddenDangerSafetyCommitteeOrPartyCommitteeResolutionRecord( + '109', + 'major_hidden_danger_safety_committee_or_party_committee_resolution_record', + ), /// 较大隐患整改-临时处置措施 - 类型: '110', 路径: 'significant_hidden_danger_rectification_temporary_disposal_measures' - significantHiddenDangerRectificationTemporaryDisposalMeasures('110', 'significant_hidden_danger_rectification_temporary_disposal_measures'), + significantHiddenDangerRectificationTemporaryDisposalMeasures( + '110', + 'significant_hidden_danger_rectification_temporary_disposal_measures', + ), /// 较大隐患整改-隐患整改过程记录 - 类型: '111', 路径: 'significant_hidden_danger_rectification_hidden_danger_rectification_process_record' - significantHiddenDangerRectificationHiddenDangerRectificationProcessRecord('111', 'significant_hidden_danger_rectification_hidden_danger_rectification_process_record'), + significantHiddenDangerRectificationHiddenDangerRectificationProcessRecord( + '111', + 'significant_hidden_danger_rectification_hidden_danger_rectification_process_record', + ), /// 补充重大隐患信息 - 类型: '112', 路径: 'supplement_major_hidden_danger_information' - supplementMajorHiddenDangerInformation('112', 'supplement_major_hidden_danger_information'), + supplementMajorHiddenDangerInformation( + '112', + 'supplement_major_hidden_danger_information', + ), /// 安委会办公室会议记录 - 类型: '113', 路径: 'safety_committee_office_meeting_record' - safetyCommitteeOfficeMeetingRecord('113', 'safety_committee_office_meeting_record'), + safetyCommitteeOfficeMeetingRecord( + '113', + 'safety_committee_office_meeting_record', + ), /// 报警图片 - 类型: '114', 路径: 'alarm_photo' alarmPhoto('114', 'alarm_photo'), /// 消防器材类型合格表中照片 - 类型: '115', 路径: 'fire_equipment_type_qualification_photo' - fireEquipmentTypeQualificationPhoto('115', 'fire_equipment_type_qualification_photo'), + fireEquipmentTypeQualificationPhoto( + '115', + 'fire_equipment_type_qualification_photo', + ), /// 动火人图片 - 类型: '116', 路径: 'hot_work_personnel_photo' hotWorkPersonnelPhoto('116', 'hot_work_personnel_photo'), @@ -126,43 +174,70 @@ enum UploadFileType { safetyPledgeSignature('117', 'safety_pledge_signature'), /// 动火单位负责人确认签字 - 类型: '118', 路径: 'hot_work_unit_responsible_person_confirmation_signature' - hotWorkUnitResponsiblePersonConfirmationSignature('118', 'hot_work_unit_responsible_person_confirmation_signature'), + hotWorkUnitResponsiblePersonConfirmationSignature( + '118', + 'hot_work_unit_responsible_person_confirmation_signature', + ), /// 现场管辖单位负责人签字 - 类型: '119', 路径: 'on_site_jurisdiction_unit_responsible_person_signature' - onSiteJurisdictionUnitResponsiblePersonSignature('119', 'on_site_jurisdiction_unit_responsible_person_signature'), + onSiteJurisdictionUnitResponsiblePersonSignature( + '119', + 'on_site_jurisdiction_unit_responsible_person_signature', + ), /// 动火许可证签发单位签字 - 类型: '120', 路径: 'hot_work_permit_issuing_unit_signature' - hotWorkPermitIssuingUnitSignature('120', 'hot_work_permit_issuing_unit_signature'), + hotWorkPermitIssuingUnitSignature( + '120', + 'hot_work_permit_issuing_unit_signature', + ), /// 动火许可证签字 - 类型: '121', 路径: 'hot_work_permit_signature' hotWorkPermitSignature('121', 'hot_work_permit_signature'), /// 动火前管辖单位确认签字 - 类型: '122', 路径: 'pre_hot_work_jurisdiction_unit_confirmation_signature' - preHotWorkJurisdictionUnitConfirmationSignature('122', 'pre_hot_work_jurisdiction_unit_confirmation_signature'), + preHotWorkJurisdictionUnitConfirmationSignature( + '122', + 'pre_hot_work_jurisdiction_unit_confirmation_signature', + ), /// 现场负责人确实签字 - 类型: '123', 路径: 'on_site_responsible_person_confirmation_signature' - onSiteResponsiblePersonConfirmationSignature('123', 'on_site_responsible_person_confirmation_signature'), + onSiteResponsiblePersonConfirmationSignature( + '123', + 'on_site_responsible_person_confirmation_signature', + ), /// 动火后现场管辖单位确认 - 类型: '124', 路径: 'post_hot_work_site_jurisdiction_unit_confirmation' - postHotWorkSiteJurisdictionUnitConfirmation('124', 'post_hot_work_site_jurisdiction_unit_confirmation'), + postHotWorkSiteJurisdictionUnitConfirmation( + '124', + 'post_hot_work_site_jurisdiction_unit_confirmation', + ), /// 延迟监火图片 - 类型: '125', 路径: 'delayed_fire_monitoring_pictures' delayedFireMonitoringPictures('125', 'delayed_fire_monitoring_pictures'), /// 安全环保检查发起签字 - 类型: '126', 路径: 'safety_and_environmental_inspection_initiation_signature' - safetyAndEnvironmentalInspectionInitiationSignature('126', 'safety_and_environmental_inspection_initiation_signature'), + safetyAndEnvironmentalInspectionInitiationSignature( + '126', + 'safety_and_environmental_inspection_initiation_signature', + ), /// 检查人确认签字 - 类型: '127', 路径: 'inspector_confirmation_signature' inspectorConfirmationSignature('127', 'inspector_confirmation_signature'), /// 被检查人确认签字 - 类型: '128', 路径: 'inspected_person_confirmation_signature' - inspectedPersonConfirmationSignature('128', 'inspected_person_confirmation_signature'), + inspectedPersonConfirmationSignature( + '128', + 'inspected_person_confirmation_signature', + ), /// 安全环保检查申辩签字 - 类型: '129', 路径: 'safety_and_environmental_inspection_appeal_signature' - safetyAndEnvironmentalInspectionAppealSignature('129', 'safety_and_environmental_inspection_appeal_signature'), + // safetyAndEnvironmentalInspectionAppealSignature('129', 'safety_and_environmental_inspection_appeal_signature'), /// (港股安委办主任)安全总监签发 - 类型: '130', 路径: 'hk_safety_committee_office_director_safety_director_issuance' - hkSafetyCommitteeOfficeDirectorSafetyDirectorIssuance('130', 'hk_safety_committee_office_director_safety_director_issuance'), + hkSafetyCommitteeOfficeDirectorSafetyDirectorIssuance( + '130', + 'hk_safety_committee_office_director_safety_director_issuance', + ), /// 动火发包单位签字 - 类型: '131', 路径: 'hot_work_contracting_unit_signature' hotWorkContractingUnitSignature('131', 'hot_work_contracting_unit_signature'), @@ -189,31 +264,55 @@ enum UploadFileType { hiddenDisposalPlan('138', 'hidden_disposal_plan'), /// 安全环保检查-检查签字 - 类型: '139', 路径: 'safety_environmental_inspection_inspection_signature' - safetyEnvironmentalInspectionInspectionSignature('139', 'safety_environmental_inspection_inspection_signature'), + safetyEnvironmentalInspectionInspectionSignature( + '139', + 'safety_environmental_inspection_inspection_signature', + ), /// 安全环保检查-检查情况 - 类型: '140', 路径: 'safety_environmental_inspection_inspection_situation' - safetyEnvironmentalInspectionInspectionSituation('140', 'safety_environmental_inspection_inspection_situation'), + safetyEnvironmentalInspectionInspectionSituation( + '140', + 'safety_environmental_inspection_inspection_situation', + ), /// 安全环保检查-检查人签字 - 类型: '141', 路径: 'safety_environmental_inspection_inspector_signature' - safetyEnvironmentalInspectionInspectorSignature('141', 'safety_environmental_inspection_inspector_signature'), + safetyEnvironmentalInspectionInspectorSignature( + '141', + 'safety_environmental_inspection_inspector_signature', + ), /// 安全环保检查-被检查人签字 - 类型: '142', 路径: 'safety_environmental_inspection_inspected_signature' - safetyEnvironmentalInspectionInspectedSignature('142', 'safety_environmental_inspection_inspected_signature'), + safetyEnvironmentalInspectionInspectedSignature( + '142', + 'safety_environmental_inspection_inspected_signature', + ), /// 安全环保检查-被检查文件 - 类型: '143', 路径: 'safety_environmental_inspection_inspected_file' - safetyEnvironmentalInspectionInspectedFile('143', 'safety_environmental_inspection_inspected_file'), + safetyEnvironmentalInspectionInspectedFile( + '143', + 'safety_environmental_inspection_inspected_file', + ), /// 安全环保检查-申辩签字 - 类型: '144', 路径: 'safety_environmental_inspection_defense_signature' - safetyEnvironmentalInspectionDefenseSignature('144', 'safety_environmental_inspection_defense_signature'), + safetyEnvironmentalInspectionDefenseSignature( + '144', + 'safety_environmental_inspection_defense_signature', + ), /// 清单检查合格 - 类型: '145', 路径: 'qualified_list_inspection' qualifiedListInspection('145', 'qualified_list_inspection'), /// 安全环保检查-验收 - 类型: '146', 路径: 'safety_environmental_inspection_acceptance' - safetyEnvironmentalInspectionAcceptance('146', 'safety_environmental_inspection_acceptance'), + safetyEnvironmentalInspectionAcceptance( + '146', + 'safety_environmental_inspection_acceptance', + ), /// 隐患清单排查查签字 - 类型: '147', 路径: 'hidden_qualified_listInspection_signature' - hiddenQualifiedListInspectionSignature('147', 'hidden_qualified_listInspection_signature'), + hiddenQualifiedListInspectionSignature( + '147', + 'hidden_qualified_listInspection_signature', + ), /// 相关方-专项资质 - 类型: '148', 路径: 'special_qualification' specialQualification('148', 'special_qualification'), @@ -234,7 +333,10 @@ enum UploadFileType { regulationsResourceLibrary('153', 'regulations_resource_library'), /// 安全生产责任制资源库 - 类型: '154', 路径: 'responsibility_based_resource_library' - responsibilityBasedResourceLibrary('154', 'responsibility_based_resource_library'), + responsibilityBasedResourceLibrary( + '154', + 'responsibility_based_resource_library', + ), /// 安全生产管理制度资源库 - 类型: '155', 路径: 'institutional_resource_library' institutionalResourceLibrary('155', 'institutional_resource_library'), @@ -246,7 +348,10 @@ enum UploadFileType { supervisionPersonnelVehicle('157', 'supervision_personnel_vehicle'), /// 监管人员车辆行驶证 - 类型: '158', 路径: 'supervision_personnel_vehicle_license' - supervisionPersonnelVehicleLicense('158', 'supervision_personnel_vehicle_license'), + supervisionPersonnelVehicleLicense( + '158', + 'supervision_personnel_vehicle_license', + ), /// 特种作业人员证件照片 - 类型: '159', 路径: 'special_operation_personnel_photo' specialOperationPersonnelPhoto('159', 'special_operation_personnel_photo'), @@ -258,7 +363,25 @@ enum UploadFileType { mainResponsiblePersonPhoto('161', 'main_responsible_person_photo'), /// 安全生产管理人员证件照片 - 类型: '162', 路径: 'photos_safety_production_management_personnel' - photosSafetyProductionManagementPersonnel('162', 'photos_safety_production_management_personnel'), + photosSafetyProductionManagementPersonnel( + '162', + 'photos_safety_production_management_personnel', + ), + + // 教培申请承诺书 - 类型: '163', 路径: 'education_and_training_application' + educationAndTrainingApplication('163', 'education_and_training_application'), + + // 课程封面 - 类型: '164', 路径: 'cover_of_education_and_training_courses' + coverOfEducationAndTrainingCourses( + '164', + 'cover_of_education_and_training_courses', + ), + + /// 在线学习签到签字 - 类型: '165', 路径: 'online_learning_sign_signature' + onlineLearningSignSignature('165', 'online_learning_sign_signature'), + + /// 在线学习考试签字 - 类型: '166', 路径: 'online_learning_exam_signature' + onlineLearningExamSignature('166', 'online_learning_exam_signature'), /// 人脸识别图片上传 - 类型: '300', 路径: 'facial_recognition_images' facialRecognitionImages('300', 'facial_recognition_images'), @@ -266,6 +389,12 @@ enum UploadFileType { /// ai识别图片 - 类型: '301', 路径: 'ai_recognition_images' aiRecognitionImages('301', 'ai_recognition_images'), + /// 消防点检检查合格 - 类型: '302', 路径: 'fire_safety_inspection_passed_images' + fireSafetyInspectionPassedImages( + '302', + 'fire_safety_inspection_passed_images', + ), + /// 门口门禁车辆行驶证照片 - 类型: '601', 路径: 'gate_access_vehicle_license_photo' gateAccessVehicleLicensePhoto('601', 'gate_access_vehicle_license_photo'), @@ -279,7 +408,10 @@ enum UploadFileType { emissionStandardCertificate('604', 'emission_standard_certificate'), /// 机动车登记证书(绿本) - 类型: '605', 路径: 'motor_vehicle_registration_certificate_green_book' - motorVehicleRegistrationCertificateGreenBook('605', 'motor_vehicle_registration_certificate_green_book'); + motorVehicleRegistrationCertificateGreenBook( + '605', + 'motor_vehicle_registration_certificate_green_book', + ); const UploadFileType(this.type, this.path); diff --git a/lib/customWidget/MultiDictValuesPicker.dart b/lib/customWidget/MultiDictValuesPicker.dart index 5374165..2b0afbc 100644 --- a/lib/customWidget/MultiDictValuesPicker.dart +++ b/lib/customWidget/MultiDictValuesPicker.dart @@ -1,6 +1,7 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/http/modules/basic_info_api.dart'; import 'package:qhd_prevention/http/ApiService.dart'; // 数据字典模型 @@ -24,20 +25,17 @@ class DictCategory { required this.parentId, required this.dictValue, required this.dictLabel, - required this.dingValue, required this.dingName, }); factory DictCategory.fromJson(Map json) { - // 安全读取并兼容字符串或数字类型的 id String parseString(dynamic v) { if (v == null) return ''; if (v is String) return v; return v.toString(); } - // 处理子节点 final rawChildren = json['children']; List childrenList = []; if (rawChildren is List) { @@ -51,7 +49,6 @@ class DictCategory { } } - // 处理扩展值 final extRaw = json['extValues']; Map extMap = {}; if (extRaw is Map) { @@ -60,7 +57,7 @@ class DictCategory { return DictCategory( id: parseString(json['id']), - name: parseString(json['dictLabel'] ?? json['name']), // 兼容新旧字段 + name: parseString(json['dictLabel'] ?? json['name']), children: childrenList, extValues: extMap, parentId: parseString(json['parentId']), @@ -71,7 +68,6 @@ class DictCategory { ); } - // 转换为Map,便于使用 Map toMap() { return { 'id': id, @@ -84,27 +80,18 @@ class DictCategory { } } -/// 数据字典选择器回调签名 typedef DictSelectCallback = void Function(String id, String name, Map? extraData); class MultiDictValuesPicker extends StatefulWidget { - /// 字典类型 final String dictType; - - /// 回调,返回选中项的 id, name 和额外数据 final DictSelectCallback onSelected; - - /// 是否显示搜索框 final bool showSearch; - - /// 标题 final String title; - - /// 确认按钮文本 final String confirmText; - - /// 取消按钮文本 final String cancelText; + final bool showIgnoreHidden; + final bool isSafeChekDangerLevel; + final bool allowSelectParent; const MultiDictValuesPicker({ Key? key, @@ -114,6 +101,9 @@ class MultiDictValuesPicker extends StatefulWidget { this.title = '请选择', this.confirmText = '确定', this.cancelText = '取消', + this.isSafeChekDangerLevel = false, + this.showIgnoreHidden = false, + this.allowSelectParent = true, }) : super(key: key); @override @@ -137,6 +127,9 @@ class _MultiDictValuesPickerState extends State { final TextEditingController _searchController = TextEditingController(); + // id -> node 映射,用于快速查父节点 + final Map _idMap = {}; + @override void initState() { super.initState(); @@ -162,9 +155,56 @@ class _MultiDictValuesPickerState extends State { }); final result = await BasicInfoApi.getDictValues(widget.dictType); - final raw = result['data'] as List; + List raw = result['data'] as List; + + if (!widget.showIgnoreHidden) { + if (widget.dictType == 'hiddenLevel') { + for (int i = 0; i < raw.length; i++) { + if (raw[i]['children'] != null) { + raw[i]['children'].removeWhere((child) => child != null && child['dictLabel'] == '忽略隐患'); + } + } + } + } + + late List resultRows = []; + if (widget.isSafeChekDangerLevel) { + if (widget.dictType == 'hiddenLevel') { + for (int i = 0; i < raw.length; i++) { + if (raw[i]['children'] != null) { + for (int m = 0; m < raw[i]['children'].length; m++) { + final ch = raw[i]['children'][m]; + if (ch != null && (ch['dictLabel'] == '轻微隐患' || ch['dictLabel'] == '一般隐患')) { + resultRows.add(ch); + } + } + } + } + } else { + resultRows = raw; + } + } else { + resultRows = raw; + } + + // 转换为 DictCategory 列表 + final loaded = resultRows.map((e) => DictCategory.fromJson(e as Map)).toList(); + + // 构建 id map(包含所有节点的扁平映射) + _idMap.clear(); + void buildIdMap(List nodes) { + for (final n in nodes) { + _idMap[n.id] = n; + if (n.children.isNotEmpty) { + buildIdMap(n.children); + } + } + } + + buildIdMap(loaded); + setState(() { - original = raw.map((e) => DictCategory.fromJson(e as Map)).toList(); + original = loaded; filtered = original; loading = false; }); @@ -181,7 +221,6 @@ class _MultiDictValuesPickerState extends State { final query = _searchController.text.toLowerCase().trim(); setState(() { filtered = query.isEmpty ? original : _filterCategories(original, query); - // 搜索时展开所有节点以便查看结果 if (query.isNotEmpty) { expandedSet.addAll(_getAllExpandableIds(filtered)); } @@ -222,10 +261,33 @@ class _MultiDictValuesPickerState extends State { return result; } + // 判断节点是否可选 + bool _isSelectableNode(DictCategory category) { + if (widget.allowSelectParent) { + return true; + } + return category.children.isEmpty; + } + + // 查找某个 id 的最上层祖先节点(若自身就是顶层则返回自身) + DictCategory? _getTopAncestor(String id) { + if (id.isEmpty) return null; + DictCategory? current = _idMap[id]; + if (current == null) return null; + // 如果没有 parentId 或 parentId 为空,当前即为顶层 + while (current != null && current.parentId.isNotEmpty) { + final parent = _idMap[current.parentId]; + if (parent == null) break; + current = parent; + } + return current; + } + Widget _buildRow(DictCategory category, int indent) { final hasChildren = category.children.isNotEmpty; final isExpanded = expandedSet.contains(category.id); final isSelected = category.id == selectedId; + final isSelectable = _isSelectableNode(category); return Column( children: [ @@ -233,16 +295,23 @@ class _MultiDictValuesPickerState extends State { onTap: () { setState(() { if (hasChildren) { - isExpanded - ? expandedSet.remove(category.id) - : expandedSet.add(category.id); + isExpanded ? expandedSet.remove(category.id) : expandedSet.add(category.id); } - selectedId = category.id; - selectedName = category.name; - selectedExtraData = category.toMap(); - if(indent==0){ - dingValueWai =category.dictValue; - dingNameWai =category.name; + if (isSelectable) { + selectedId = category.id; + selectedName = category.name; + selectedExtraData = category.toMap(); + + // 计算最上层父节点(如果存在) + final top = _getTopAncestor(category.id); + if (top != null) { + dingValueWai = top.dictValue; + dingNameWai = top.name; + } else { + // 回退到自身值(防御) + dingValueWai = category.dictValue; + dingNameWai = category.name; + } } }); }, @@ -255,9 +324,7 @@ class _MultiDictValuesPickerState extends State { width: 24, child: hasChildren ? Icon( - isExpanded - ? Icons.arrow_drop_down_rounded - : Icons.arrow_right_rounded, + isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded, size: 35, color: Colors.grey[600], ) @@ -283,19 +350,18 @@ class _MultiDictValuesPickerState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Icon( - isSelected - ? Icons.radio_button_checked - : Icons.radio_button_unchecked, + child: isSelectable + ? Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: Colors.blue, - ), + ) + : null, ), ], ), ), ), - if (hasChildren && isExpanded) - ...category.children.map((child) => _buildRow(child, indent + 1)), + if (hasChildren && isExpanded) ...category.children.map((child) => _buildRow(child, indent + 1)), ], ); } @@ -307,10 +373,7 @@ class _MultiDictValuesPickerState extends State { child: Center( child: Text( widget.title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), ); @@ -322,41 +385,38 @@ class _MultiDictValuesPickerState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ - // 取消按钮 GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Text( - widget.cancelText, - style: const TextStyle(fontSize: 16, color: Colors.grey), - ), + child: Text(widget.cancelText, style: const TextStyle(fontSize: 16, color: Colors.grey)), ), ), - - // 搜索框(如果有搜索功能) if (widget.showSearch) ...[ Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), - child: SearchBarWidget( - controller: _searchController, - isShowSearchButton: false, - onSearch: (keyboard) {}, - ), + child: SearchBarWidget(controller: _searchController, isShowSearchButton: false, onSearch: (keyboard) {}), ), ), - ] else ...[ + ] else const Expanded(child: SizedBox()), - ], - - // 确定按钮 GestureDetector( onTap: selectedId.isEmpty ? null : () { - selectedExtraData?['dingValue']=dingValueWai ; - selectedExtraData?['dingName']=dingNameWai ; + // 在返回之前,将最上层父节点信息也回传(保证 selectedExtraData 存在) + final top = _getTopAncestor(selectedId); + if (top != null) { + selectedExtraData ??= {}; + selectedExtraData!['dingValue'] = top.dictValue; + selectedExtraData!['dingName'] = top.name; + } else { + selectedExtraData ??= {}; + selectedExtraData!['dingValue'] = dingValueWai; + selectedExtraData!['dingName'] = dingNameWai; + } + Navigator.of(context).pop(); widget.onSelected(selectedId, selectedName, selectedExtraData); }, @@ -382,11 +442,7 @@ class _MultiDictValuesPickerState extends State { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('加载中...'), - ], + children: [CircularProgressIndicator(), SizedBox(height: 16), Text('加载中...')], ), ); } @@ -400,16 +456,9 @@ class _MultiDictValuesPickerState extends State { const SizedBox(height: 16), const Text('加载失败', style: TextStyle(fontSize: 16)), const SizedBox(height: 8), - Text( - errorMessage, - style: const TextStyle(fontSize: 12, color: Colors.grey), - textAlign: TextAlign.center, - ), + Text(errorMessage, style: const TextStyle(fontSize: 12, color: Colors.grey), textAlign: TextAlign.center), const SizedBox(height: 16), - ElevatedButton( - onPressed: _loadDictData, - child: const Text('重试'), - ), + CustomButton(text: '重试', onPressed: _loadDictData), ], ), ); @@ -419,11 +468,7 @@ class _MultiDictValuesPickerState extends State { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.search_off, size: 48, color: Colors.grey), - SizedBox(height: 16), - Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey)), - ], + children: [Icon(Icons.search_off, size: 48, color: Colors.grey), SizedBox(height: 16), Text('暂无数据', style: TextStyle(fontSize: 16, color: Colors.grey))], ), ); } @@ -437,6 +482,23 @@ class _MultiDictValuesPickerState extends State { ); } + Widget _buildHintMessage() { + if (!widget.allowSelectParent) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: Colors.blue[50], + child: Row( + children: [ + Icon(Icons.info_outline, size: 16, color: Colors.blue[700]), + const SizedBox(width: 8), + Expanded(child: Text('只能选择没有子项的节点', style: TextStyle(fontSize: 12, color: Colors.blue[700]))), + ], + ), + ); + } + return const SizedBox.shrink(); + } + @override Widget build(BuildContext context) { return Container( @@ -444,32 +506,16 @@ class _MultiDictValuesPickerState extends State { height: MediaQuery.of(context).size.height * 0.7, decoration: BoxDecoration( color: Colors.white, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, -2), - ), - ], + borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, -2))], ), child: Column( children: [ - // 标题行 - // _buildTitleBar(), - // 操作行(取消、搜索、确定) _buildActionBar(), const Divider(height: 1), - - // 内容区域 - Expanded( - child: _buildContent(), - ), + Expanded(child: _buildContent()), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/customWidget/department_picker.dart b/lib/customWidget/department_picker.dart index b4b0613..8d323eb 100644 --- a/lib/customWidget/department_picker.dart +++ b/lib/customWidget/department_picker.dart @@ -12,6 +12,8 @@ class Category { final Map extValues; final String departmentId; final String parentId; + final String corpinfoName; + final String corpinfoId; final List childrenList; Category({ @@ -21,6 +23,8 @@ class Category { required this.extValues, required this.departmentId, required this.parentId, + required this.corpinfoName, + required this.corpinfoId, }); factory Category.fromJson(Map json) { @@ -59,18 +63,34 @@ class Category { extValues: extMap, departmentId: parseString(json['departmentId']), parentId: parseString(json['parentId']), + corpinfoName: parseString(json['corpinfoName']), + corpinfoId: parseString(json['corpinfoId']), ); } + Map toJson() { + return { + 'id': id, + 'name': name, + 'childrenList': childrenList.map((e) => e.toJson()).toList(), + 'extValues': extValues, + 'departmentId': departmentId, + 'parentId': parentId, + 'corpinfoName': corpinfoName, + 'corpinfoId': corpinfoId, + }; + } } /// 弹窗回调签名:返回选中项的 id 和 name -typedef DeptSelectCallback = void Function(String id, String name); +typedef DeptSelectCallback = void Function(String id, String name, Map data); class DepartmentPicker extends StatefulWidget { /// 回调,返回选中部门 id 与 name final DeptSelectCallback onSelected; + /// 默认选中的部门 + final Map? data; - const DepartmentPicker({Key? key, required this.onSelected}) + const DepartmentPicker({Key? key, required this.onSelected, this.data}) : super(key: key); @override @@ -80,6 +100,8 @@ class DepartmentPicker extends StatefulWidget { class _DepartmentPickerState extends State { String selectedId = ''; String selectedName = ''; + Map selectedData = {}; + Set expandedSet = {}; List original = []; @@ -108,7 +130,7 @@ class _DepartmentPickerState extends State { Future _loadData() async { try { - final result = await BasicInfoApi.getDeptTree({}); + final result = await BasicInfoApi.getDeptTree(widget.data!); final raw = result['data'] as List; print(raw); setState(() { @@ -141,6 +163,8 @@ class _DepartmentPickerState extends State { extValues: cat.extValues, departmentId: cat.departmentId, parentId: cat.parentId, + corpinfoName: cat.corpinfoName, + corpinfoId: cat.corpinfoId, ), ); } @@ -164,6 +188,7 @@ class _DepartmentPickerState extends State { } selectedId = cat.id; selectedName = cat.name; + selectedData = cat.toJson(); }); }, child: Container( @@ -240,7 +265,7 @@ class _DepartmentPickerState extends State { GestureDetector( onTap: () { Navigator.of(context).pop(); - widget.onSelected(selectedId, selectedName); + widget.onSelected(selectedId, selectedName, selectedData); }, child: const Text( '确定', diff --git a/lib/customWidget/search_bar_widget.dart b/lib/customWidget/search_bar_widget.dart index 187c7cf..cba39d8 100644 --- a/lib/customWidget/search_bar_widget.dart +++ b/lib/customWidget/search_bar_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; class SearchBarWidget extends StatefulWidget { final String hintText; @@ -89,7 +90,11 @@ class _SearchBarWidgetState extends State { mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: 12), - Image.asset('assets/images/search.png', height: 15, width: 15,) + Image.asset( + 'assets/images/search.png', + height: 15, + width: 15, + ), ], ), // 控制 prefixIcon 的约束,保证图标不会撑高 @@ -114,48 +119,23 @@ class _SearchBarWidgetState extends State { ), const SizedBox(width: 10), if (widget.isShowSearchButton) - SizedBox( - height: innerHeight - 4, // 稍微比 TextField 矮一点看着更协调 - child: ElevatedButton( - onPressed: () => widget.onSearch(widget.controller.text.trim()), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - elevation: 4, - shadowColor: Colors.black45, - ), - child: Text( - widget.buttonText, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), - ), - ), - if (widget.showResetButton) const SizedBox(width: 10), - if (widget.showResetButton) - SizedBox( + CustomButton( + text: widget.buttonText, height: innerHeight - 4, - child: ElevatedButton( - onPressed: () { - updateText(''); - widget.onReset?.call(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - padding: const EdgeInsets.symmetric(horizontal: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - elevation: 1, - shadowColor: Colors.black26, - ), - child: Text( - widget.resetButtonText, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), - ), + padding: EdgeInsets.symmetric(horizontal: 15), + onPressed: () => widget.onSearch(widget.controller.text.trim()), + ), + + if (widget.showResetButton) + CustomButton( + text: widget.resetButtonText, + buttonStyle:ButtonStyleType.secondary, + height: innerHeight - 4, + padding: EdgeInsets.symmetric(horizontal: 15), + onPressed: () { + updateText(''); + widget.onReset?.call(); + }, ), ], ); diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart index 0e4af02..5897631 100644 --- a/lib/http/HttpManager.dart +++ b/lib/http/HttpManager.dart @@ -152,7 +152,7 @@ extension HttpManagerUpload on HttpManager { required Map fromData, CancelToken? cancelToken, }) async { - fromData['corpinfoId'] = '1983773013086048256'; + // fromData['corpinfoId'] = '1983773013086048256'; final form = FormData.fromMap(fromData); final token = SessionService.instance.token ?? ''; diff --git a/lib/http/modules/auth_api.dart b/lib/http/modules/auth_api.dart index 9361e4a..9c79503 100644 --- a/lib/http/modules/auth_api.dart +++ b/lib/http/modules/auth_api.dart @@ -13,6 +13,15 @@ class AuthApi { data: {...data}, ); } + /// 未入职前验证是否入职,返回值与登录相同 + static Future> userLoginCheckFirm(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/getUserCorpByPhone', + method: Method.post, + data: {...data}, + ); + } /// 本地获取验证码 static Future> getUserCaptcha() { return HttpManager().request( diff --git a/lib/http/modules/basic_info_api.dart b/lib/http/modules/basic_info_api.dart index daa9519..0395997 100644 --- a/lib/http/modules/basic_info_api.dart +++ b/lib/http/modules/basic_info_api.dart @@ -210,5 +210,14 @@ class CertificateApi { data: {}, ); } + // 删除证照验证 + static Future> deleteCertificateVerify(String eqUserId) { + return HttpManager().request( + ApiService.basePath, + '/xgfManager/project/projectHasUser?eqUserId=$eqUserId', + method: Method.get, + data: {}, + ); + } } diff --git a/lib/http/modules/edu_api.dart b/lib/http/modules/edu_api.dart new file mode 100644 index 0000000..e06e0e8 --- /dev/null +++ b/lib/http/modules/edu_api.dart @@ -0,0 +1,109 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; + +class EduApi { + /// 班级列表 + static Future> getClassList(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/class/list', + method: Method.post, + data: { + ...data, + }, + ); + } + // 验证是否可以签到 + static Future> checkSignIn(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentSign/verify', + method: Method.post, + data: { + ...data, + }, + ); + } + // 对比人脸 + static Future> compareFace(Map formData, String path) async { + final data = Map.from(formData); + // 把文件路径填成 MultipartFile + data['files'] = await MultipartFile.fromFile( + path, + filename: path.split(Platform.pathSeparator).last, + ); + return HttpManager().uploadImages( + baseUrl: '${ApiService.basePath}/edu', + path: '/app/studentSign/compareFace', + fromData: data, + ); + } + // 上传签字照片 + static Future> uploadSignature(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentSign/uploadSignUrl', + method: Method.post, + data: { + ...data, + }, + ); + } + // 签到信息列表 + static Future> getSignInList(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentSign/listAll', + method: Method.post, + data: { + ...data, + }, + ); + } + // 获取试卷详情 + static Future> getExamDetail(String classId) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/classExamPaper/getInfoByClassId/$classId', + method: Method.get, + data: { + }, + ); + } + // 提交试卷 + static Future> submitExam(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentExamRecord/submit', + method: Method.post, + data: { + ...data, + }, + ); + } + // 获取考试记录 + static Future> getExamRecord(Map data) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentExamRecord/list', + method: Method.post, + data: { + ...data, + }, + ); + } + // 获取考试记录详情 + static Future> getExamRecordDetail(String recordId) async { + return HttpManager().request( + '${ApiService.basePath}/edu', + '/app/studentExamRecord/$recordId', + method: Method.get, + data: { + }, + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/Study/signin_information_list_page.dart b/lib/pages/home/Study/signin_information_list_page.dart new file mode 100644 index 0000000..51fe316 --- /dev/null +++ b/lib/pages/home/Study/signin_information_list_page.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/modules/edu_api.dart'; +import 'package:qhd_prevention/pages/home/scan_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.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'; +class SigninInformationListPage extends StatefulWidget { + const SigninInformationListPage({Key? key, required this.info}) + : super(key: key); + final Map info; + @override + _SigninInformationListPageState createState() => _SigninInformationListPageState(); +} + +class _SigninInformationListPageState extends State { + // 列表数据与分页 + List list = []; + int currentPage = 1; + int rows = 10; + int totalPage = 1; + bool isLoading = false; + + + final TextEditingController _searchController = TextEditingController(); + int sindex = 0; // 筛选索引(只有发起模式可见) + String searchKeywords = ''; + Map flowData = {}; + final GlobalKey _scaffoldKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _fetchData(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent && + !isLoading) { + if (currentPage < totalPage) { + currentPage++; + _fetchData(); + } + } + } + + Future _fetchData() async { + if (isLoading) return; + setState(() => isLoading = true); + + try { + final data = { + 'pageSize': rows, + 'pageIndex': currentPage, + 'classId': widget.info['classId'] ?? '', + 'studentId': widget.info['studentId'] ?? '', + 'phone': SessionService.instance.userName ?? '', + }; + final response = await EduApi.getSignInList(data); + + setState(() { + final newData = response['data'] as List? ?? []; + if (currentPage == 1) { + list = newData; + } else { + list.addAll(newData); + } + totalPage = response['totalPages'] ?? 1; + isLoading = false; + }); + } catch (e) { + print('Error fetching data: $e'); + setState(() => isLoading = false); + ToastUtil.showNormal(context, '获取列表失败'); + } + } + + void _search() { + searchKeywords = _searchController.text.trim(); + currentPage = 1; + list.clear(); + _fetchData(); + } + // 重置 + void _reset() { + searchKeywords = ''; + currentPage = 1; + list.clear(); + _fetchData(); + } + + Widget _buildListItem(Map item) { + int type = item["type"] ?? 1; + + return Card( + color: Colors.white, + margin: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + + }, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + + ), + child: Image.network( + '${ApiService.baseImgPath}${item['faceUrl'] ?? ''}', // 假设头像URL字段名为avatarUrl,根据实际情况调整 + width: 100, + // height: 60, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + // 如果网络图片加载失败,则显示默认的圆形文字头像 + return CircleAvatar( + radius: 5, + backgroundColor: Colors.grey[300], + child: Text( + item['createName']?.substring(0, 1) ?? '', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ); + }, + ), + ), + const SizedBox(width: 10), + Column( + spacing: 5, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + item['createName'] ?? '', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + // const SizedBox(height: 5,), + Text("班级名称: ${widget.info['name'] ?? ''}"), + Text("签到时间: ${item["updateTime"] ?? ''}"), + Text("培训地点: ${item["trainingLocation"] ?? ''}"), + Row( + children: [ + Text("状态:"), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + color: Color(0xFF94F0BD), + borderRadius: BorderRadius.circular(3), + ), + child: Text("${type == 1 ? '认证签到' : '考试签到'}", style: TextStyle(color: Colors.green),) + + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ], + ) + ), + ), + ); + } + + + Widget _buildListContent() { + if (isLoading && list.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } else if (list.isEmpty) { + return NoDataWidget.show(); + } else { + return ListView.builder( + padding: EdgeInsets.zero, + controller: _scrollController, + itemCount: list.length + (isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + return _buildListItem(list[index]); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: MyAppbar( + title: '班级管理', + ), + body: SafeArea( + child: Column( + children: [ + // List + Expanded(child: _buildListContent()), + ], + ), + ), + ); + } + +} diff --git a/lib/pages/home/Study/study_class_list_page.dart b/lib/pages/home/Study/study_class_list_page.dart new file mode 100644 index 0000000..aca1be0 --- /dev/null +++ b/lib/pages/home/Study/study_class_list_page.dart @@ -0,0 +1,405 @@ +import 'dart:convert'; + +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/ApiService.dart'; +import 'package:qhd_prevention/http/modules/edu_api.dart'; +import 'package:qhd_prevention/pages/home/Study/signin_information_list_page.dart'; +import 'package:qhd_prevention/pages/home/Study/study_exam_record_list.dart'; +import 'package:qhd_prevention/pages/home/Study/study_take_exam_page.dart'; +import 'package:qhd_prevention/pages/home/scan_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/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.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'; + +/// 9种页面模式 +class StudyClassListPage extends StatefulWidget { + const StudyClassListPage({Key? key}) : super(key: key); + + @override + _StudyClassListPageState createState() => _StudyClassListPageState(); +} + +class _StudyClassListPageState extends State { + // 列表数据与分页 + List list = []; + int currentPage = 1; + int rows = 10; + int totalPage = 1; + bool isLoading = false; + + final TextEditingController _searchController = TextEditingController(); + int sindex = 0; // 筛选索引(只有发起模式可见) + String searchKeywords = ''; + Map flowData = {}; + final GlobalKey _scaffoldKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _fetchData(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent && + !isLoading) { + if (currentPage < totalPage) { + currentPage++; + _fetchData(); + } + } + } + + Future _fetchData() async { + if (isLoading) return; + setState(() => isLoading = true); + + try { + final data = { + 'pageSize': rows, + 'pageIndex': currentPage, + 'phone': SessionService.instance.userName ?? '', + 'likeName': searchKeywords, + }; + final response = await EduApi.getClassList(data); + + setState(() { + final newData = response['data'] as List? ?? []; + if (currentPage == 1) { + list = newData; + } else { + list.addAll(newData); + } + totalPage = response['totalPages'] ?? 1; + isLoading = false; + }); + } catch (e) { + print('Error fetching data: $e'); + setState(() => isLoading = false); + ToastUtil.showNormal(context, '获取列表失败'); + } + } + + // 根据状态返回状态名1-未申请 2-待开班 3- 培训中 4-培训结束 + String getStatusName(int status) { + switch (status) { + case 1: + return '未申请'; + case 2: + return '待开班'; + case 3: + return '培训中'; + case 4: + return '培训结束'; + default: + return ''; + } + } + + void _search() { + searchKeywords = _searchController.text.trim(); + currentPage = 1; + list.clear(); + _fetchData(); + } + + // 重置 + void _reset() { + searchKeywords = ''; + currentPage = 1; + list.clear(); + _fetchData(); + } + + /// 考试记录 + Future _handleApply(bool isInPlan, Map planItem) async { + await pushPage(StudyExamRecordList(info: planItem,), context); + + _search(); + } + + /// 根据当前页面模式返回对应的操作按钮 + List _buildActionButtons(Map item) { + final List buttons = []; + + buttons.addAll([ + CustomButton( + text: '签到信息', + height: 35, + backgroundColor: Colors.blue, + onPressed: () async { + await pushPage(SigninInformationListPage(info: item), context); + }, + ), + CustomButton( + text: '考试记录', + height: 35, + backgroundColor: Colors.blue, + onPressed: () => _handleApply(true, item), + ), + ]); + + return buttons; + } + + Widget _buildListItem(Map item) { + final actionButtons = _buildActionButtons(item); + + // 构建一行内均分按钮并在按钮间插入间距 10 + List buttonRowChildren = []; + for (int i = 0; i < actionButtons.length; i++) { + // 将按钮包在 SizedBox 以保证高度一致,同时用 Expanded 占满剩余宽度 + buttonRowChildren.add( + Expanded(child: SizedBox(height: 35, child: actionButtons[i])), + ); + if (i != actionButtons.length - 1) { + buttonRowChildren.add(const SizedBox(width: 10)); + } + } + return Card( + color: Colors.white, + margin: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + item['name'] ?? '', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 5), + Text("所属单位: ${item['corpName'] ?? ''}"), + Text("培训开始时间: ${item["startTime"] ?? ''}"), + Text("培训结束时间: ${item["endTime"] ?? ''}"), + Text("班级状态: ${getStatusName(item['state'])}"), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: buttonRowChildren, + ), + ], + ), + ), + ), + ); + } + + Widget _buildListContent() { + if (isLoading && list.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } else if (list.isEmpty) { + return NoDataWidget.show(); + } else { + return ListView.builder( + padding: EdgeInsets.zero, + controller: _scrollController, + itemCount: list.length + (isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + return _buildListItem(list[index]); + }, + ); + } + } + + void startScan() async { + final result = await pushPage(ScanPage(type: ScanType.SignIn), context); + if (result == null) { + 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'] ?? '签到失败'); + } + } + + // 上传签到签字 + Future _signUpload(Map data, int type) 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); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: MyAppbar( + title: '班级管理', + actions: [ + 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, + ), + ), + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + child: Row( + children: [ + Expanded( + child: SearchBarWidget( + showResetButton: true, + hintText: "请输入班级名称", + onSearch: (text) { + _search(); + }, + onReset: () { + _search(); + }, + controller: _searchController, + ), + ), + ], + ), + ), + const Divider(height: 1), + // List + Expanded(child: _buildListContent()), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/Study/study_exam_record_detail.dart b/lib/pages/home/Study/study_exam_record_detail.dart new file mode 100644 index 0000000..a17a198 --- /dev/null +++ b/lib/pages/home/Study/study_exam_record_detail.dart @@ -0,0 +1,294 @@ +// lib/pages/study_exam_record_detail.dart +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/http/modules/edu_api.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +import '../../../tools/tools.dart'; + +class StudyExamRecordDetail extends StatefulWidget { + final String examId; + + const StudyExamRecordDetail({Key? key, required this.examId,}) : super(key: key); + + @override + _StudyExamRecordDetailState createState() => _StudyExamRecordDetailState(); +} + +class Question { + final String questionDry; // 题干 + final String questionType; // 题型('1','2','3','4') + final Map options; // 选项 A/B/C/D(若存在) + final String answer; // 正确答案(参考答案) + String choiceAnswer; // 用户作答(可能为空) + final int score; // 分值(如有) + final String descr; // 解析 / 说明 + bool answered; // 是否已作答(用于高亮/对错显示) + + Question({ + required this.questionDry, + required this.questionType, + required this.options, + required this.answer, + required this.choiceAnswer, + required this.score, + required this.descr, + this.answered = true, + }); + + + factory Question.fromJson(Map json) { + // 将 questionType 转换为整数,然后转回字符串以保持一致的比较逻辑 + String type = (json['questionType'] ?? 1).toString(); + final opts = {}; + if (type == '1' || type == '2' || type == '3') { + opts['A'] = json['optionA'] ?? ''; + opts['B'] = json['optionB'] ?? ''; + if (type != '3') { + opts['C'] = json['optionC'] ?? ''; + opts['D'] = json['optionD'] ?? ''; + } + } + + final qDry = json['questionDry'] ?? ''; + final userAnswer = json['choiceAnswer'] ?? ''; + final correct = json['answer'] ?? ''; + + final scoreVal = () { + final val = json['score']; + if (val is int) return val; + if (val is String) { + final parsed = int.tryParse(val); + return parsed ?? 0; + } + return 0; + }(); + print('$userAnswer、$correct'); + final descr = json['descr'] ?? json['explain'] ?? ''; + + return Question( + questionDry: qDry, + questionType: type, + options: opts, + answer: correct, + choiceAnswer: userAnswer, + score: scoreVal, + descr: descr, + answered: true, + ); + } +} + +class _StudyExamRecordDetailState extends State { + bool loading = true; + List questions = []; + Map paperInfo = {}; + int current = 0; + final Map questionTypeMap = { + '1': '单选题', + '2': '多选题', + '3': '判断题', + '4': '填空题', + }; + + @override + void initState() { + super.initState(); + _fetchData(); + } + + Future _fetchData() async { + setState(() => loading = true); + final res = await EduApi.getExamRecordDetail(widget.examId); + if (res['success']) { + paperInfo = res['data']; + + var list = paperInfo['examRecordItemList'] as List? ?? []; + questions = list.map((e) { + if (e is Map) return Question.fromJson(e); + return Question.fromJson(Map.from(e)); + }).toList(); + // 标记所有题目为已作答,直接显示对错(接口是考试记录,默认已答) + for (var q in questions) { + q.answered = true; + } + } else { + questions = []; + paperInfo = {}; + } + setState(() => loading = false); + } + + Widget _buildOptions(Question q) { + if (q.questionType == '4') { + // 填空题:显示用户作答(choiceAnswer) + return TextField( + controller: TextEditingController(text: q.choiceAnswer), + readOnly: true, + maxLength: 255, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ); + } + + List keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D']; + return Column( + children: keys.map((key) { + final isChecked = (q.choiceAnswer ?? '').contains(key); + final isCorrect = (q.answer ?? '').contains(key); + final right = q.answered && isCorrect && isChecked; // 用户选且正确 + final err = q.answered && !isCorrect && isChecked; // 用户选但错误 + final warn = q.answered && isCorrect && !isChecked; // 正确但用户未选(提示) + return Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: right + ? Colors.green + : err + ? Colors.red + : warn + ? Colors.green + : Colors.black12, + shape: BoxShape.circle, + ), + child: Text( + key, + style: TextStyle( + color: (right || err || warn) ? Colors.white : Colors.black87, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + q.options[key] ?? '', + style: TextStyle( + color: right + ? Colors.green + : err + ? Colors.red + : warn + ? Colors.green + : Colors.black87, + fontSize: 16, + ), + ), + ), + ], + ), + ); + }).toList(), + ); + } + + String _renderAnswerText(Question q) { + // 对错题(判断题)通常用 A/B 或者 true/false + if (q.questionType == '3') { + // 若 choiceAnswer 是 'A' 表示“对”,'B' 表示“错”,兼容其他写法 + final ans = (q.choiceAnswer ?? '').toUpperCase(); + if (ans == 'A' || ans == 'TRUE' || ans == 'T') return '对'; + if (ans == 'B' || ans == 'FALSE' || ans == 'F') return '错'; + return q.choiceAnswer; + } + return q.choiceAnswer; + } + + @override + Widget build(BuildContext context) { + final q = questions.isNotEmpty ? questions[current] : null; + return Scaffold( + appBar: MyAppbar(title: '考试记录'), + body: loading + ? const Center(child: CircularProgressIndicator()) + : questions.isEmpty + ? NoDataWidget.show() + : Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头部背景 & 进度 + Stack( + children: [ + Container( + width: double.infinity, + height: 100, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/study/bgimg1.png'), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + Positioned.fill( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${paperInfo['examName'] ?? ''}', + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), + ), + const Padding(padding: EdgeInsets.symmetric(horizontal: 15), child: Divider(color: Colors.white30, height: 20)), + Text( + '当前试题 ${current + 1}/${questions.length}', + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + ], + )), + ), + ], + ), + const SizedBox(height: 16), + // 题干与选项 + if (q != null) ...[ + Text( + '${current + 1}. ${q.questionDry} (${questionTypeMap[q.questionType] ?? ''})', + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 16), + _buildOptions(q), + const Divider(), + Text('我的答案: ${_renderAnswerText(q)}'), + Text('正确答案: ${q.answer}'), + const SizedBox(height: 8), + Text('权威解读: ${q.descr}'), + ], + const Spacer(), + // 底部翻页按钮 + Row( + children: [ + if (current > 0) + Expanded( + child: CustomButton( + text: '上一题', + backgroundColor: const Color(0xFFD7D7D7), + textColor: Colors.black54, + onPressed: () => setState(() => current--), + ), + ), + if (current > 0 && current < questions.length - 1) const SizedBox(width: 16), + if (current < questions.length - 1) + Expanded( + child: CustomButton( + text: '下一题', + backgroundColor: Colors.blue, + onPressed: () => setState(() => current++), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/Study/study_exam_record_list.dart b/lib/pages/home/Study/study_exam_record_list.dart new file mode 100644 index 0000000..dc29792 --- /dev/null +++ b/lib/pages/home/Study/study_exam_record_list.dart @@ -0,0 +1,219 @@ +import 'dart:convert'; + +import 'package:flutter/material.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/http/modules/edu_api.dart'; +import 'package:qhd_prevention/pages/home/Study/study_exam_record_detail.dart'; +import 'package:qhd_prevention/pages/home/scan_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class StudyExamRecordList extends StatefulWidget { + const StudyExamRecordList({Key? key, required this.info}) : super(key: key); + final Map info; + + @override + _StudyExamRecordListState createState() => _StudyExamRecordListState(); +} + +class _StudyExamRecordListState extends State { + // 列表数据与分页 + List list = []; + int currentPage = 1; + int rows = 10; + int totalPage = 1; + bool isLoading = false; + + final TextEditingController _searchController = TextEditingController(); + int sindex = 0; // 筛选索引(只有发起模式可见) + String searchKeywords = ''; + Map flowData = {}; + final GlobalKey _scaffoldKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _fetchData(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent && + !isLoading) { + if (currentPage < totalPage) { + currentPage++; + _fetchData(); + } + } + } + + Future _fetchData() async { + if (isLoading) return; + setState(() => isLoading = true); + + try { + final data = { + 'pageSize': rows, + 'pageIndex': currentPage, + 'likeStudentId': widget.info['studentId'] ?? '', + }; + final response = await EduApi.getExamRecord(data); + + setState(() { + final newData = response['data'] as List? ?? []; + if (currentPage == 1) { + list = newData; + } else { + list.addAll(newData); + } + totalPage = response['totalPages'] ?? 1; + isLoading = false; + }); + } catch (e) { + print('Error fetching data: $e'); + setState(() => isLoading = false); + ToastUtil.showNormal(context, '获取列表失败'); + } + } + + void _search() { + searchKeywords = _searchController.text.trim(); + currentPage = 1; + list.clear(); + _fetchData(); + } + + // 重置 + void _reset() { + searchKeywords = ''; + currentPage = 1; + list.clear(); + _fetchData(); + } + + // 详情 + Future _getExamDetail(Map item) async { + pushPage(StudyExamRecordDetail(examId: item['id'] ?? ''), context); + } + + Widget _buildListItem(Map item) { + int type = item["type"] ?? 1; + + return Card( + color: Colors.white, + margin: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () { + _getExamDetail(item); + }, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 5, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item['examName'] ?? '试卷名称', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + spacing: 5, + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text("考试时间: "), Text("分数: "), Text("考试情况: ")], + ), + const SizedBox(width: 30), + Column( + spacing: 5, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("${item['examTimeBegin'] ?? ''}"), + Text("${item["examScore"] ?? ''}"), + Text("${item["result"] == 1 ? '通过' : '未通过'}"), + ], + ), + ], + ), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // const SizedBox(), + // CustomButton( + // text: '查看', + // padding: EdgeInsetsGeometry.symmetric(horizontal: 40), + // height: 35, + // onPressed: () { + // _getExamDetail(item); + // }, + // ), + const SizedBox(height: 5), + CustomButton( + text: '查看', + padding: EdgeInsetsGeometry.symmetric(horizontal: 40), + height: 35, + onPressed: () { + _getExamDetail(item); + }, + // ] + ), + ], + ), + ), + ), + ); + } + + Widget _buildListContent() { + if (isLoading && list.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } else if (list.isEmpty) { + return NoDataWidget.show(); + } else { + return ListView.builder( + padding: EdgeInsets.zero, + controller: _scrollController, + itemCount: list.length + (isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: CircularProgressIndicator()), + ); + } + return _buildListItem(list[index]); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: MyAppbar(title: '考试记录'), + body: SafeArea( + child: Column( + children: [ + // List + Expanded(child: _buildListContent()), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/Study/study_tab_list_page.dart b/lib/pages/home/Study/study_tab_list_page.dart new file mode 100644 index 0000000..35bb276 --- /dev/null +++ b/lib/pages/home/Study/study_tab_list_page.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/IconBadgeButton.dart'; +import 'package:qhd_prevention/pages/home/Study/study_class_list_page.dart'; +import 'package:qhd_prevention/pages/home/scan_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/smart_image.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:qhd_prevention/common/route_aware_state.dart'; + +class StudyTabListPage extends StatefulWidget { + const StudyTabListPage({super.key}); + + @override + State createState() => _StudyTabListPageState(); +} + +class _StudyTabListPageState extends RouteAwareState { + late List> buttonInfos = [ + { + "icon": "assets/images/icon_study01.png", + "title": "培训管理", + "unreadCount": 0, + } + + ]; + + @override + void initState() { + super.initState(); + } + Future onVisible() async { + _getData(); + } + Future _getData() async { + // final data = await HiddenDangerApi.specialCheckWork(); + // setState(() { + // final eight_work_count = data['count'] ?? {}; + // buttonInfos = [ + // { + // "icon": "assets/icon-apps/icon-807.png", + // "title": "动火作业", + // "unreadCount": eight_work_count['HOTWORK_COUNT'], + // } + // ]; + // }); + } + + void _handleIconTap(int index) async { + switch (index) { + case 0: + await pushPage(StudyClassListPage(), context); + break; + } + _getData(); + } + + // @override + // Widget build(BuildContext context) { + // return Scaffold( + // appBar: MyAppbar(title: '特殊作业'), + // body: SafeArea( + // child: WorkTabIconGrid( + // buttonInfos: buttonInfos, + // onItemPressed: _handleItemPressed, + // ), + // ), + // ); + // } + + + @override + Widget build(BuildContext context) { + double bannerHeight = 730/1125 * MediaQuery.of(context).size.width; + const double iconSectionHeight = 150.0; + const double iconOverlapBanner = 30.0; // 图标区覆盖 banner 的高度 + return PopScope( + canPop: true, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: MyAppbar( + title: '培训管理', + backgroundColor: Colors.transparent, + + ), + body: ListView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.zero, + children: [ + SizedBox( + height: bannerHeight + iconSectionHeight, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + height: bannerHeight, + child: _buildBannerSection(bannerHeight), + ), + Positioned( + left: 10, + right: 10, + top: bannerHeight - iconOverlapBanner, + height: iconSectionHeight, + child: _buildIconSection(context), + ), + ], + ), + ), + ], + ), + ), + ); + + } + // 构建顶部 Banner + Widget _buildBannerSection(double bannerHeight) { + return Stack( + children: [ + // 背景图片 + Image.asset( + "assets/images/study_banner.png", + width: MediaQuery.of(context).size.width, + height: bannerHeight, + fit: BoxFit.fitWidth, + ), + ], + ); + } + Widget _buildIconSection(BuildContext context) { + 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: Column( + children: [ + _buildIconRow(startIndex: 0), + + ], + ), + ); + } + 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) { + 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), + ); + } + +} diff --git a/lib/pages/home/Study/study_take_exam_page.dart b/lib/pages/home/Study/study_take_exam_page.dart new file mode 100644 index 0000000..3089ff5 --- /dev/null +++ b/lib/pages/home/Study/study_take_exam_page.dart @@ -0,0 +1,458 @@ +// lib/pages/study_take_exam_page.dart +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.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/modules/edu_api.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'dart:convert'; + +import 'package:qhd_prevention/tools/tools.dart'; + +class Question { + final String questionDry; + final String questionType; + final String questionId; + final String answer; // 正确答案(如果接口返回) + final double score; + final Map options; + + // 用户选择的答案(UI 中变化) + String choiceAnswer; + + Question({ + required this.questionDry, + required this.questionType, + required this.questionId, + required this.answer, + required this.score, + required this.options, + this.choiceAnswer = '', + }); + + factory Question.fromJson(Map json) { + final type = '${json['questionType'] ?? ''}'; + final opts = {}; + if (type == '1' || type == '2' || type == '3') { + opts['A'] = json['optionA'] as String? ?? ''; + opts['B'] = json['optionB'] as String? ?? ''; + if (type != '3') { + opts['C'] = json['optionC'] as String? ?? ''; + opts['D'] = json['optionD'] as String? ?? ''; + } + } + // 保留原接口回传的参考答案(若有) + final answer = json['answer'] as String? ?? ''; + final qDry = json['questionDry'] as String? ?? ''; + final qId = json['questionId'] as String? ?? ''; + double score = json['score']; + + return Question( + questionDry: qDry, + questionType: type, + questionId: qId, + answer: answer, + score: score, + options: opts, + ); + } + + /// 构造提交给后端的最小字段对象 + Map toSubmitJson() { + return { + 'questionId': questionId, + 'answer': answer, + 'choiceAnswer': choiceAnswer, + 'score': score, + }; + } +} + +class StudyTakeExamPage extends StatefulWidget { + const StudyTakeExamPage({ + required this.examInfo, + required this.signInfo, + super.key, + }); + + final Map examInfo; + final Map signInfo; + + @override + State createState() => _StudyTakeExamPageState(); +} + +class _StudyTakeExamPageState extends State { + late final List questions; + late final Map info; + int current = 0; + late int remainingSeconds; + Timer? _timer; + final String _startExamTime = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); + + final questionTypeMap = { + '1': '单选题', + '2': '多选题', + '3': '判断题', + '4': '填空题', + }; + + @override + void initState() { + super.initState(); + info = widget.examInfo as Map? ?? {}; + final rawList = widget.examInfo['questionList'] as List? ?? []; + questions = rawList.map((e) => Question.fromJson(e as Map)).toList(); + final minutes = info['examTime'] as int? ?? 0; + remainingSeconds = minutes * 60; + _startTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Future _showTip(String content) { + return CustomAlertDialog.showAlert( + context, + title: '温馨提示', + content: content, + confirmText: '确认', + ); + } + + bool _validateCurrentAnswer() { + final q = questions[current]; + if (q.choiceAnswer.isEmpty) { + _showTip('请对本题进行作答。'); + return false; + } + if (q.questionType == '2' && q.choiceAnswer.split(',').length < 2) { + _showTip('多选题最少需要选择两个答案。'); + return false; + } + return true; + } + + void _startTimer() { + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (remainingSeconds <= 0) { + timer.cancel(); + _onTimeUp(); + } else { + setState(() => remainingSeconds--); + } + }); + } + + /// 重置页面以便“继续考试” + void _resetForRetry() { + // 清空每题的选择 + for (var q in questions) { + q.choiceAnswer = ''; + } + // 重置当前题索引、计时等 + setState(() { + current = 0; + final minutes = info['examTime'] as int? ?? 0; + remainingSeconds = minutes * 60; + }); + // 重新启动计时器 + _startTimer(); + } + + void _chooseTopic(String type, String key) { + final q = questions[current]; + if (type == 'radio' || type == 'judge') { + q.choiceAnswer = (q.choiceAnswer == key) ? '' : key; + } else { + final chars = q.choiceAnswer.isEmpty ? [] : q.choiceAnswer.split(','); + if (chars.contains(key)) { + chars.remove(key); + } else { + chars.add(key); + } + chars.sort(); + q.choiceAnswer = chars.join(','); + } + setState(() {}); + } + + void _nextQuestion() { + if (!_validateCurrentAnswer()) return; + if (current < questions.length - 1) setState(() => current++); + } + + void _previousQuestion() { + if (current > 0) setState(() => current--); + } + + void _confirmSubmit() async { + if (!_validateCurrentAnswer()) return; + final ok = await CustomAlertDialog.showConfirm( + context, + title: '温馨提示', + content: '请确认是否交卷!', + cancelText: '取消', + ); + if (ok) { + _submit(); + } + } + + Future _submit() async { + LoadingDialogHelper.show(message: '正在提交'); + + // 按原逻辑:若多选题需要去掉逗号(保留你的业务要求) + for (var q in questions) { + if (q.questionType == '2') { + // 如果后端期待无逗号格式的话保留这一步,否则可删 + q.choiceAnswer = q.choiceAnswer.replaceAll(',', ''); + } + } + + // 构造真正的 JSON 数组(而不是 JSON 字符串) + final questionList = questions.map((q) => q.toSubmitJson()).toList(); + + final data = { + 'studentId': widget.signInfo['studentId'] ?? '', + 'classId': widget.examInfo['classId'] ?? '', + 'corpinfoId': SessionService.instance.tenantId ?? '', + 'classExamPaperId': widget.examInfo['classExamPaperId'] ?? '', + 'examPaperId': widget.examInfo['examPaperId'] ?? '', + 'studentSignId': widget.signInfo['studentSignId'], + 'examTimeBegin': _startExamTime, + // 当前时间就是结束时间 + 'examTimeEnd': DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), + 'questionList': questionList, // 这里放真正的 List> + }; + + final res = await EduApi.submitExam(data); + LoadingDialogHelper.hide(); + + if (res['success']) { + final data = res['data'] as Map? ?? {}; + final score = data['examScore'] ?? 0; + final passed = data['result'] == 1; + + // 弹窗告诉用户结果:通过直接返回上一页;未通过给“继续考试 / 确定”两个按钮 + final result = await CustomAlertDialog.showConfirm( + context, + title: '温馨提示', + content: passed + ? '您的成绩为 $score 分,恭喜您通过本次考试,请继续保持!' + : '您的成绩为 $score 分,很遗憾您没有通过本次考试,请再接再厉!', + cancelText: passed ? '' : '继续考试', + confirmText: '确定', + ); + + // 如果考试通过或用户点击了“确定”,则返回上一页 + if (passed || result) { + Navigator.of(context).pop(); + return; + } + + // 到这里说明:未通过且用户点击了“继续考试” -> 重置页面状态并继续考试 + _resetForRetry(); + } else { + // 可选:处理失败情况并提示错误信息 + final msg = res['message'] ?? '提交失败,请重试'; + ToastUtil.showError(context, msg); + } + } + + void _onTimeUp() { + ToastUtil.showError(context, '考试时间已结束'); + _submit(); + } + + Widget _buildOptions(Question q) { + if (q.questionType == '4') { + return TextField( + controller: TextEditingController(text: q.choiceAnswer), + onChanged: (val) => q.choiceAnswer = val, + maxLength: 255, + decoration: const InputDecoration( + hintText: '请输入内容', + border: OutlineInputBorder(), + ), + ); + } + final keys = q.questionType == '3' ? ['A', 'B'] : ['A', 'B', 'C', 'D']; + return Column( + children: keys.map((key) { + final active = q.choiceAnswer.split(',').contains(key); + return GestureDetector( + onTap: () => _chooseTopic( + q.questionType == '3' ? 'judge' : (q.questionType == '2' ? 'multiple' : 'radio'), + key, + ), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? Colors.blue : Colors.grey.shade200, + shape: BoxShape.circle, + ), + child: Text( + key, + style: TextStyle( + color: active ? Colors.white : Colors.black87, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + q.options[key] ?? '', + style: const TextStyle(fontSize: 15), + ), + ), + ], + ), + ), + ); + }).toList(), + ); + } + + String get _formattedTime { + final m = remainingSeconds ~/ 60; + final s = remainingSeconds % 60; + return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + final q = questions.isNotEmpty ? questions[current] : null; + return PopScope( + canPop: true, // 禁用返回 + child: Scaffold( + backgroundColor: Colors.white, + appBar: const MyAppbar(title: '课程考试', isBack: false), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: double.infinity, + height: 120, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/study/bgimg1.png'), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + Positioned.fill( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '试卷名称:${widget.examInfo['examName'] ?? ''}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + '当前试题 ${current + 1}/${questions.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + ), + ), + const SizedBox(height: 8), + Text( + '考试剩余时间:$_formattedTime', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 16), + if (q != null) + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${current + 1}. ${q.questionDry} (${questionTypeMap[q.questionType] ?? ''})', + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + const SizedBox(height: 16), + _buildOptions(q), + const SizedBox(height: 24), + ], + ), + ), + ) + else + const Expanded(child: SizedBox()), + const SizedBox(height: 8), + Row( + children: [ + if (current > 0) + Expanded( + child: CustomButton( + text: '上一题', + backgroundColor: const Color(0xFFD7D7D7), + textColor: Colors.black54, + onPressed: _previousQuestion, + ), + ), + if (current > 0 && current < questions.length - 1) const SizedBox(width: 16), + if (current < questions.length - 1) + Expanded( + child: CustomButton( + text: '下一题', + backgroundColor: Colors.blue, + onPressed: _nextQuestion, + ), + ), + if (current == questions.length - 1) + Expanded( + child: CustomButton( + text: '交卷', + backgroundColor: Colors.blue, + onPressed: _confirmSubmit, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 1447494..dc8e665 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -5,12 +5,17 @@ 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/http/ApiService.dart'; +import 'package:qhd_prevention/pages/home/Study/study_tab_list_page.dart'; import 'package:qhd_prevention/pages/home/scan_page.dart'; import 'package:qhd_prevention/pages/home/unit/unit_tab_page.dart'; import 'package:qhd_prevention/pages/main_tab.dart'; +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/tools/h_colors.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -31,11 +36,16 @@ class HomePageState extends RouteAwareState int _currentPage = 0; bool _isMobileSelected = true; // 切换按钮状态 - void startScan() { - Navigator.push( + void startScan() async { + + final result = await pushPage( + ScanPage(type: ScanType.Onboarding), context, - MaterialPageRoute(builder: (_) => ScanPage(type: ScanType.Onboarding)), ); + if (result == null) { + return; + } + pushPage(OnboardingFullPage(scanData: result), context); } // 缓存 key @@ -152,10 +162,13 @@ class HomePageState extends RouteAwareState bool _initialLoadingHidden = true; bool _firstLoad = false; + /// 记录显示登录弹窗状态 + bool _isShowCheckLogin = false; + @override void initState() { super.initState(); - + _isShowCheckLogin = widget.isChooseFirm; // _getNeedSafetyCommitment(); _buttonVisibility = List.filled(buttonInfos.length, true); @@ -175,6 +188,8 @@ class HomePageState extends RouteAwareState curve: Curves.easeInOut, ); } catch (_) {} + _getNeedSafetyCommitment(); + setState(() {}); }); @@ -187,6 +202,47 @@ class HomePageState extends RouteAwareState }); } + /// 校验是否入职 + Future _getNeedSafetyCommitment() async { + if (_isShowCheckLogin) { + return; + } + final prefs = await SharedPreferences.getInstance(); + final phone = prefs.getString('savePhone') ?? ''; + final pwd = prefs.getString('savePass') ?? ''; + final result = await AuthApi.userLoginCheckFirm({'phone': phone}); + if (result['success']) { + final resData = result['data']; + 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(); + if (res['success'] == true) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const MainPage(isChooseFirm: true), + ), + ); + } + }, + ); + } else { + _isShowCheckLogin = false; + } + } + } + void _onScroll() { final offset = _scrollController.hasClients ? _scrollController.offset : 0.0; @@ -292,6 +348,7 @@ class HomePageState extends RouteAwareState bannerHeight - iconOverlapBanner + iconSectionHeight + 60; final double statusBar = MediaQuery.of(context).padding.top; + final double indicatorCoverHeight = statusBar + 80.0; // 80 可根据需要调整 return PopScope( canPop: false, @@ -299,7 +356,10 @@ class HomePageState extends RouteAwareState extendBodyBehindAppBar: true, body: Stack( children: [ + RefreshIndicator( + backgroundColor: Colors.white, // <- 关键:把背景设为不透明 + color: Colors.blue, // 进度条颜色 onRefresh: _onRefresh, child: ListView( controller: _scrollController, @@ -360,12 +420,10 @@ class HomePageState extends RouteAwareState ), const SizedBox(height: 20), - ] - + ], ], ), ), - // 浮动 AppBar(平滑出现/隐藏),放在 ListView 之上 Positioned( top: 0, @@ -396,7 +454,7 @@ class HomePageState extends RouteAwareState opacity: _showFloatingAppBar ? 1.0 : 0.0, child: SizedBox( height: _floatingBarHeight, - child: const SizedBox() + child: const SizedBox(), ), ), ), @@ -405,15 +463,16 @@ class HomePageState extends RouteAwareState Positioned( top: statusBar + 14, width: screenWidth(context), - child: Center(child: - Text( - '秦港-相关方安全管理', - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w600, - color: Colors.white, + child: Center( + child: Text( + '秦港-相关方安全管理', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w600, + color: Colors.white, + ), ), - ),) + ), ), // 固定在最上层的图标(位于 AppBar 之上),保证它们不会随滚动移动 _buildFixedTopIcons(context), @@ -424,7 +483,7 @@ class HomePageState extends RouteAwareState } Future _joinFirm() async { - pushPage(FirmListPage(isBack: true,), context); + pushPage(FirmListPage(isBack: true), context); } // 固定在屏幕右上角的图标(不会随页面滚动) @@ -435,32 +494,32 @@ class HomePageState extends RouteAwareState return Positioned( top: topOffset, - right: 12, + right: 8, child: Row( children: [ GestureDetector( onTap: startScan, child: Container( - width: 38, - height: 38, + width: 30, + height: 30, alignment: Alignment.center, child: Image.asset( "assets/icon-apps/home_saoyisao.png", - width: 22, - height: 22, + width: 20, + height: 20, ), ), ), GestureDetector( onTap: _joinFirm, child: Container( - width: 38, - height: 38, + width: 30, + height: 30, alignment: Alignment.center, child: Image.asset( "assets/icon-apps/home_add.png", - width: 22, - height: 22, + width: 20, + height: 20, ), ), ), @@ -568,60 +627,71 @@ class HomePageState extends RouteAwareState BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)), ], ), - 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()); - } - }), - ), + 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), + 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); - }, - ) - ], - ), - ), + // 第二行(仍然 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( @@ -674,6 +744,7 @@ class HomePageState extends RouteAwareState case "口门门禁": break; case "入港培训": + pushPage(StudyTabListPage(), context); break; default: break; diff --git a/lib/pages/home/scan_page.dart b/lib/pages/home/scan_page.dart index 684c755..8050ac3 100644 --- a/lib/pages/home/scan_page.dart +++ b/lib/pages/home/scan_page.dart @@ -15,6 +15,8 @@ import 'package:qhd_prevention/services/SessionService.dart'; enum ScanType { /// 入职 Onboarding, + // 签到 + SignIn, } class ScanPage extends StatefulWidget { @@ -60,10 +62,13 @@ class _ScanPageState extends State { } void _showResult(String result) { - if (widget.type == ScanType.Onboarding) { - final json = jsonDecode(result); - Navigator.pop(context, json); - } + final json = jsonDecode(result); + Navigator.pop(context, json); + // if (widget.type == ScanType.Onboarding) { + // //{\"corpinfoName\":\"相关方测试单位\",\"corpinfoId\":\"74f140f388ec40f091e8e7afc6f1175e\",\"id\":\"2008789163605929984\"} + // final json = jsonDecode(result); + // Navigator.pop(context, json); + // } } diff --git a/lib/pages/home/unit/unit_quit_apply_page.dart b/lib/pages/home/unit/unit_quit_apply_page.dart index 6bd5456..d62217e 100644 --- a/lib/pages/home/unit/unit_quit_apply_page.dart +++ b/lib/pages/home/unit/unit_quit_apply_page.dart @@ -79,7 +79,7 @@ class _UnitQuitApplyPageState extends State { const Divider(), ItemListWidget.singleLineTitleText( label: '申请人', - text: SessionService.instance.name, + text: SessionService.instance.userData?.name, isEditable: false, ), const Divider(), diff --git a/lib/pages/home/unit/unit_tab_page.dart b/lib/pages/home/unit/unit_tab_page.dart index a06c7c2..1a8ac31 100644 --- a/lib/pages/home/unit/unit_tab_page.dart +++ b/lib/pages/home/unit/unit_tab_page.dart @@ -57,7 +57,7 @@ class _UnitTabPageState extends RouteAwareState { const double iconSectionHeight = 150.0; const double iconOverlapBanner = 30.0; // 图标区覆盖 banner 的高度 return PopScope( - canPop: false, + canPop: true, child: Scaffold( extendBodyBehindAppBar: true, appBar: MyAppbar( diff --git a/lib/pages/main_tab.dart b/lib/pages/main_tab.dart index 78efd48..121197c 100644 --- a/lib/pages/main_tab.dart +++ b/lib/pages/main_tab.dart @@ -41,8 +41,6 @@ class _MainPageState extends State with WidgetsBindingObserver { late List _pages; late List _tabVisibility; // 存储每个Tab的显示状态 - final List _titles = ['首页', '通知', '我的']; - @override void initState() { super.initState(); @@ -55,7 +53,7 @@ class _MainPageState extends State with WidgetsBindingObserver { MinePage(key: _mineKey, isChooseFirm: widget.isChooseFirm), ]; // 启动心跳服务 - // HeartbeatService().start(); + HeartbeatService().start(); } @override diff --git a/lib/pages/home/certificate/certificate_detail_page.dart b/lib/pages/mine/certificate/certificate_detail_page.dart similarity index 87% rename from lib/pages/home/certificate/certificate_detail_page.dart rename to lib/pages/mine/certificate/certificate_detail_page.dart index c55b3db..3768a54 100644 --- a/lib/pages/home/certificate/certificate_detail_page.dart +++ b/lib/pages/mine/certificate/certificate_detail_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:qhd_prevention/constants/app_enums.dart'; +import 'package:qhd_prevention/customWidget/MultiDictValuesPicker.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/customWidget/item_list_widget.dart'; @@ -9,13 +10,14 @@ import 'package:qhd_prevention/customWidget/picker/CupertinoDatePicker.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/http/modules/basic_info_api.dart'; -import 'package:qhd_prevention/pages/home/certificate/certificate_list_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/customWidget/bottom_picker.dart'; import 'package:qhd_prevention/services/SessionService.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'dart:io'; +import 'certificate_list_page.dart'; + enum CertifitcateTypeMode { // 特种设备 specialEquipment('tzsbczry', 2), @@ -95,11 +97,14 @@ class _CertificateDetailPageState extends State { if (pd['type'] == CertifitcateTypeMode.principal.value) { _chooseMode = CertifitcateTypeMode.principal; } + if (pd['type'] == CertifitcateTypeMode.safetyManager.value) { + _chooseMode = CertifitcateTypeMode.safetyManager; + } if (pd['type'] == CertifitcateTypeMode.specialEquipment.value) { _chooseMode = CertifitcateTypeMode.specialEquipment; for (Map item in _equipmentList) { if (item['dictValue'] == pd['assignmentOperatingItemsCode']) { - _chooseEquipmentTypeList = item['children']; + _chooseEquipmentTypeList = item['children'] ?? []; break; } } @@ -108,7 +113,7 @@ class _CertificateDetailPageState extends State { _chooseMode = CertifitcateTypeMode.specialWorker; for (Map item in _specialWorkList) { if (item['dictValue'] == pd['industryCategoryCode']) { - _chooseWorkTypeList = item['children']; + _chooseWorkTypeList = item['children'] ?? []; break; } } @@ -295,7 +300,7 @@ class _CertificateDetailPageState extends State { pd['userCertificateId'] = result['data']['foreignKey'] ?? ''; } return true; - }else{ + } else { ToastUtil.showNormal(context, result['errMessage'] ?? '保存失败'); } } catch (e) { @@ -323,10 +328,10 @@ class _CertificateDetailPageState extends State { ToastUtil.showNormal(context, '请填写证书编号'); return false; } - if (_chooseMode == CertifitcateTypeMode.principal || - _chooseMode == CertifitcateTypeMode.safetyManager) { + if (!(_chooseMode == CertifitcateTypeMode.specialEquipment || + _chooseMode == CertifitcateTypeMode.specialWorker)) { if (!FormUtils.hasValue(pd, 'postName')) { - ToastUtil.showNormal(context, '请填写岗位名称'); + ToastUtil.showNormal(context, '请选择岗位'); return false; } } @@ -366,10 +371,14 @@ class _CertificateDetailPageState extends State { ToastUtil.showNormal(context, '请选择有效期结束时间'); return false; } - if (!FormUtils.hasValue(pd, 'reviewDate')) { - ToastUtil.showNormal(context, '请选择复审时间'); - return false; + if (_chooseMode == CertifitcateTypeMode.specialWorker || + _chooseMode == CertifitcateTypeMode.specialEquipment) { + if (!FormUtils.hasValue(pd, 'reviewDate')) { + ToastUtil.showNormal(context, '请选择复审时间'); + return false; + } } + return true; } @@ -388,7 +397,7 @@ class _CertificateDetailPageState extends State { } return Scaffold( appBar: MyAppbar( - title: widget.model == CertifitcateEditMode.edit ? '证书信息添加' : '查看信息', + title: widget.model == CertifitcateEditMode.add ? '证书信息添加' : '查看信息', isBack: true, ), body: SafeArea( @@ -477,17 +486,35 @@ class _CertificateDetailPageState extends State { }, ), const Divider(), - if (!(_chooseMode == CertifitcateTypeMode.specialEquipment || - _chooseMode == CertifitcateTypeMode.specialWorker)) ...[ - ItemListWidget.singleLineTitleText( + if (_chooseMode == CertifitcateTypeMode.principal || + _chooseMode == CertifitcateTypeMode.safetyManager) ...[ + ItemListWidget.selectableLineTitleTextRightButton( label: '岗位名称:', - isRequired: _isEdit, - text: pd['postName'] ?? '', - - hintText: '请输入岗位名称', isEditable: _isEdit, - onChanged: (value) { - pd['postName'] = value; + text: pd['postName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: Colors.black54, + backgroundColor: Colors.transparent, + builder: + (_) => MultiDictValuesPicker( + title: '岗位名称', + dictType: + _chooseMode == CertifitcateTypeMode.principal + ? 'zyfzrgwmc0000' + : 'aqscglrygwmc0000', + allowSelectParent: false, + onSelected: (id, name, extraData) { + setState(() { + pd['postId'] = extraData?['dictValue']; + pd['postName'] = name; + }); + }, + ), + ); }, ), @@ -510,7 +537,35 @@ class _CertificateDetailPageState extends State { if (_chooseMode == CertifitcateTypeMode.specialWorker) ...[ ItemListWidget.selectableLineTitleTextRightButton( - label: '行业类型:', + label: '操作项目:', + isEditable: _isEdit, + text: pd['industryOperatingItemsName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + if (_chooseWorkTypeList.isEmpty) { + ToastUtil.showNormal(context, '暂无操作项目'); + return; + } + final found = await BottomPicker.show( + context, + items: _chooseWorkTypeList, + itemBuilder: + (i) => Text( + i['dictLabel']!, + textAlign: TextAlign.center, + ), + initialIndex: 0, + ); + //FocusHelper.clearFocus(context); + if (found != null) { + pd['industryOperatingItemsName'] = found['dictLabel']; + pd['industryOperatingItemsCode'] = found['dictValue']; + } + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '行业类别:', isEditable: _isEdit, text: pd['industryCategoryName'] ?? '请选择', isRequired: _isEdit, @@ -530,7 +585,7 @@ class _CertificateDetailPageState extends State { setState(() { pd['industryCategoryName'] = found['dictLabel']; pd['industryCategoryCode'] = found['dictValue']; - _chooseWorkTypeList = found['children']; + _chooseWorkTypeList = found['children'] ?? []; pd['industryOperatingItemsName'] = ''; pd['industryOperatingItemsCode'] = ''; }); @@ -538,30 +593,7 @@ class _CertificateDetailPageState extends State { }, ), const Divider(), - ItemListWidget.selectableLineTitleTextRightButton( - label: '操作项目:', - isEditable: _isEdit, - text: pd['industryOperatingItemsName'] ?? '请选择', - isRequired: _isEdit, - onTap: () async { - final found = await BottomPicker.show( - context, - items: _chooseWorkTypeList, - itemBuilder: - (i) => Text( - i['dictLabel']!, - textAlign: TextAlign.center, - ), - initialIndex: 0, - ); - //FocusHelper.clearFocus(context); - if (found != null) { - pd['industryOperatingItemsName'] = found['dictLabel']; - pd['industryOperatingItemsCode'] = found['dictValue']; - } - }, - ), - const Divider(), + ], if (_chooseMode == CertifitcateTypeMode.specialEquipment) ...[ ItemListWidget.selectableLineTitleTextRightButton( @@ -585,7 +617,7 @@ class _CertificateDetailPageState extends State { setState(() { pd['assignmentOperatingItemsName'] = found['dictLabel']; pd['assignmentOperatingItemsCode'] = found['dictValue']; - _chooseEquipmentTypeList = found['children']; + _chooseEquipmentTypeList = found['children'] ?? []; pd['assignmentCategoryName'] = ''; pd['assignmentCategoryCode'] = ''; }); @@ -594,11 +626,15 @@ class _CertificateDetailPageState extends State { ), const Divider(), ItemListWidget.selectableLineTitleTextRightButton( - label: '行业类型:', + label: '作业类别:', isEditable: _isEdit, text: pd['assignmentCategoryName'] ?? '请选择', isRequired: _isEdit, onTap: () async { + if (_chooseEquipmentTypeList.isEmpty) { + ToastUtil.showNormal(context, '暂无行业类型'); + return; + } final found = await BottomPicker.show( context, items: _chooseEquipmentTypeList, @@ -652,6 +688,14 @@ class _CertificateDetailPageState extends State { pd['certificateDateStart'] = DateFormat( 'yyyy-MM-dd', ).format(picked); + // 如果已经选择结束时间,且结束时间小于开始时间,则结束时间清空 + if (FormUtils.hasValue(pd, 'certificateDateEnd') && + isAfterStr( + pd['certificateDateStart'], + pd['certificateDateEnd'], + )) { + pd['certificateDateEnd'] = ''; + } }); //FocusHelper.clearFocus(context); } @@ -663,10 +707,12 @@ class _CertificateDetailPageState extends State { isEditable: _isEdit, text: pd['certificateDateEnd'] ?? '请选择', isRequired: _isEdit, + onTap: () async { DateTime? picked = await BottomDateTimePicker.showDate( mode: BottomPickerMode.date, context, + minTimeStr: pd['certificateDateStart'], ); if (picked != null) { setState(() { diff --git a/lib/pages/home/certificate/certificate_list_page.dart b/lib/pages/mine/certificate/certificate_list_page.dart similarity index 81% rename from lib/pages/home/certificate/certificate_list_page.dart rename to lib/pages/mine/certificate/certificate_list_page.dart index b7324a8..0bfd6cd 100644 --- a/lib/pages/home/certificate/certificate_list_page.dart +++ b/lib/pages/mine/certificate/certificate_list_page.dart @@ -6,7 +6,6 @@ import 'package:qhd_prevention/customWidget/item_list_widget.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/ApiService.dart'; import 'package:qhd_prevention/http/modules/safety_check_api.dart'; -import 'package:qhd_prevention/pages/home/certificate/certificate_detail_page.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/services/SessionService.dart'; import 'package:qhd_prevention/tools/tools.dart'; @@ -14,6 +13,8 @@ import 'package:qhd_prevention/customWidget/bottom_picker.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'certificate_detail_page.dart'; + enum CertifitcateEditMode { add, detail, edit } class CertificateListPage extends StatefulWidget { @@ -72,19 +73,23 @@ class _CertificateListPageState extends State { final response = await CertificateApi.getCertificateList(data); setState(() { - final newData = response['data'] as List? ?? []; - if (currentPage == 1) { - list = newData; - } else { - list.addAll(newData); + if (response['success']) { + final newData = response['data'] as List? ?? []; + if (currentPage == 1) { + list = newData; + } else { + list.addAll(newData); + } + totalPage = response['totalPage'] ?? 1; + }else{ + ToastUtil.showNormal(context, response['errMessage']); } - totalPage = response['totalPage'] ?? 1; + isLoading = false; }); } catch (e) { print('Error fetching data: $e'); setState(() => isLoading = false); - ToastUtil.showNormal(context, '获取列表失败'); } } @@ -110,26 +115,38 @@ class _CertificateListPageState extends State { /// 删除证照 Future _deleteCertificate(Map item) async { - await CustomAlertDialog.showAlert( - context, - title: '温馨提示', - content: '确定要删除此证书吗?', - onConfirm: () async { - try { - final response = await CertificateApi.deleteCertificate(item['id']); - if (response['success'] == true) { - ToastUtil.showNormal(context, '删除成功'); - currentPage = 1; - _fetchData(); - } else { - ToastUtil.showNormal(context, response['errMessage'] ?? '删除失败'); + + LoadingDialogHelper.show(); + final result = await CertificateApi.deleteCertificateVerify(item['userId'] ?? ''); + LoadingDialogHelper.dismiss(); + if (result['success']) { + final dataList = result['data'] ?? []; + final projectList = dataList.map((item) { + return item['projectName']; + }).toList(); + + await CustomAlertDialog.showConfirm( + context, + title: '提示', + content: '【${SessionService.instance.userData?.name}】有正在进行的项目,包含【${projectList.join(',')}】确定删除吗?\n删除可能会对正在进行的项目造成异常状态,请谨慎操作。', + confirmText: '删除', + onConfirm: () async { + try { + final response = await CertificateApi.deleteCertificate(item['id']); + if (response['success'] == true) { + ToastUtil.showNormal(context, '删除成功'); + currentPage = 1; + _fetchData(); + } else { + ToastUtil.showNormal(context, response['errMessage'] ?? '删除失败'); + } + } catch (e) { + print('Error fetching data: $e'); + ToastUtil.showNormal(context, '删除失败'); } - } catch (e) { - print('Error fetching data: $e'); - ToastUtil.showNormal(context, '删除失败'); } - }, - ); + ); + } } Widget _buildListItem(Map item) { diff --git a/lib/pages/mine/face_ecognition_page.dart b/lib/pages/mine/face_ecognition_page.dart index e553e56..a450486 100644 --- a/lib/pages/mine/face_ecognition_page.dart +++ b/lib/pages/mine/face_ecognition_page.dart @@ -47,7 +47,7 @@ class _FaceRecognitionPageState extends State String _errMsg = ''; bool get _isManualMode => - (widget.mode == FaceMode.setUpdata || widget.mode == FaceMode.initSave); + (widget.mode == FaceMode.setUpdata || widget.mode == FaceMode.initSave || widget.mode == FaceMode.study); bool _isInitializing = false; bool _isTaking = false; @@ -322,11 +322,12 @@ class _FaceRecognitionPageState extends State } catch (_) {} final XFile pic = await _cameraController!.takePicture(); - if (widget.mode == FaceMode.initSave) { + if (widget.mode == FaceMode.initSave || widget.mode == FaceMode.study) { _hideLoading(); _onSuccess(pic.path); return; } + final raw = await FileApi.uploadFile( pic.path, UploadFileType.facialRecognitionImages, @@ -372,7 +373,7 @@ class _FaceRecognitionPageState extends State } Future.delayed(const Duration(milliseconds: 800), () { - if (mounted) Navigator.of(context).pop(true); + if (mounted) Navigator.of(context).pop(filePath); }); } diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart index 6635717..bb21103 100644 --- a/lib/pages/mine/mine_page.dart +++ b/lib/pages/mine/mine_page.dart @@ -5,7 +5,6 @@ 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/certificate/certificate_list_page.dart'; import 'package:qhd_prevention/pages/home/scan_page.dart'; import 'package:qhd_prevention/pages/home/userinfo_page.dart'; import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; @@ -19,6 +18,8 @@ import 'package:qhd_prevention/services/SessionService.dart'; import 'package:qhd_prevention/tools/tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'certificate/certificate_list_page.dart'; + class MinePage extends StatefulWidget { const MinePage({super.key, required this.isChooseFirm}); final bool isChooseFirm; @@ -50,11 +51,24 @@ class MinePageState extends State { void initState() { // TODO: implement initState super.initState(); - name = SessionService.instance.name ?? "登录/注册"; - phone = SessionService.instance.phone ?? ""; - } - @override + _getUserInfo(); + } + // 获取用户信息 + Future _getUserInfo() async { + final res = await BasicInfoApi.getUserMessage( + '${SessionService.instance.accountId}', + ); + if (res['success'] == true) { + final data = res['data'] as Map; + SessionService.instance.updateFromApiResponse(data); + await SessionService.instance.saveToPrefs(); + setState(() { + name = SessionService.instance.userData?.name ?? "登录/注册"; + phone = SessionService.instance.userData?.phone ?? ""; + }); + } + } @override Widget build(BuildContext context) { final double headerHeight = 300.0; @@ -132,7 +146,8 @@ class MinePageState extends State { children: [ Padding( padding: EdgeInsets.fromLTRB(0, 0, 0, 10), - child: Image.asset( + child: + Image.asset( "assets/images/my_bg.png", width: MediaQuery.of(context).size.width, // 获取屏幕宽度 fit: BoxFit.cover, @@ -228,6 +243,8 @@ class MinePageState extends State { } Widget _buildSloganSection() { + final headerUrl = SessionService.instance.userData?.userAvatarUrl ?? ''; + return Container( margin: EdgeInsets.fromLTRB(0, 100, 0, 0), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), @@ -237,10 +254,16 @@ class MinePageState extends State { children: [ Row( children: [ - CircleAvatar( - backgroundImage: AssetImage("assets/images/my_bg.png"), - radius: 30, - ), + headerUrl.isEmpty + ? const CircleAvatar( + backgroundImage: AssetImage("assets/images/my_bg.png"), + radius: 30, + ) + : CircleAvatar( + backgroundImage: NetworkImage(ApiService.baseImgPath + headerUrl), + radius: 30, + ), + const SizedBox(width: 16), Text( name, @@ -279,11 +302,12 @@ class MinePageState extends State { title: "我的信息", icon: "assets/images/ico9.png", value: notificationsEnabled, - onChanged: (value) { - pushPage( + onChanged: (value) async { + await pushPage( FullUserinfoPage(isEidt: false, isChooseFirm: true), context, ); + }, ), @@ -339,12 +363,15 @@ class MinePageState extends State { }, ), - _buildSettingItem( - title: "问题反馈", - icon: "assets/images/ico13.png", - value: passwordChanged, - onChanged: (value) => setState(() => passwordChanged = value!), - ), + // _buildSettingItem( + // title: "问题反馈", + // icon: "assets/images/ico13.png", + // value: passwordChanged, + // onChanged: (value) { + // ToastUtil.showNormal(context, '需求待定'); + // // pushPage(FeedbackPage(), context); + // }, + // ), // const Divider(height: 1, indent: 60), _buildSettingItem( @@ -365,7 +392,7 @@ class MinePageState extends State { if (widget.isChooseFirm) _buildSettingItem( title: "切换账户", - icon: "assets/images/ico15.png", + icon: "assets/images/ico_switch.png", value: logoutSelected, onChanged: (value) { pushPage(MineChangeFirmPage(), context); @@ -373,7 +400,7 @@ class MinePageState extends State { ), _buildSettingItem( title: "账户注销", - icon: "assets/images/ico15.png", + icon: "assets/images/ico_quit.png", value: logoutSelected, onChanged: (value) { _logout(); diff --git a/lib/pages/mine/mine_set_pwd_page.dart b/lib/pages/mine/mine_set_pwd_page.dart index 3536545..c0e72b5 100644 --- a/lib/pages/mine/mine_set_pwd_page.dart +++ b/lib/pages/mine/mine_set_pwd_page.dart @@ -6,6 +6,7 @@ import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/pages/user/login_page.dart'; import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../http/ApiService.dart'; @@ -131,7 +132,10 @@ class _MineSetPwdPageState extends State { final oldPwd = _oldPwdController.text.trim(); final newPwd = _newPwdController.text.trim(); final confirmPwd = _confirmPwdController.text.trim(); - + // if (oldPwd == newPwd) { + // ToastUtil.showNormal(context, '新密码不能喝旧密码相同'); + // return; + // } if (newPwd != confirmPwd) { ToastUtil.showNormal(context, '新密码和确认密码两次输入的密码不一致'); return; @@ -151,11 +155,11 @@ class _MineSetPwdPageState extends State { ToastUtil.showNormal(context, '新密码必须包含大小写字母、数字和特殊符号。'); return; } - await _changePass(oldPwd, newPwd, confirmPwd); } Future _changePass(String oldPwd, String newPwd, String confirmPwd) async { + LoadingDialogHelper.show(); try { passData['id'] = SessionService.instance.accountId ?? ""; passData['password'] = oldPwd; @@ -163,7 +167,7 @@ class _MineSetPwdPageState extends State { passData['confirmPassword'] = confirmPwd; final raw = await AuthApi.changePassWord(passData); - + LoadingDialogHelper.hide() ; if (raw['success'] == true) { ToastUtil.showNormal(context, '新密码修改成功!'); Navigator.pop(context, true); @@ -181,6 +185,7 @@ class _MineSetPwdPageState extends State { ToastUtil.showNormal(context, raw['errMessage'] ?? '请求失败,请重试'); } } catch (e) { + LoadingDialogHelper.hide() ; print('修改密码出错:$e'); ToastUtil.showNormal(context, '请求失败,请重试'); } diff --git a/lib/pages/mine/onboarding_full_page.dart b/lib/pages/mine/onboarding_full_page.dart index 0014d75..58f146d 100644 --- a/lib/pages/mine/onboarding_full_page.dart +++ b/lib/pages/mine/onboarding_full_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:qhd_prevention/customWidget/bottom_picker.dart'; import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/department_picker.dart'; import 'package:qhd_prevention/customWidget/item_list_widget.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/http/modules/basic_info_api.dart'; @@ -31,26 +32,31 @@ class _OnboardingFullPageState extends State { @override void initState() { super.initState(); - _getDept(); } - // 获取部门 - Future _getDept() async { - try { - final data = { - 'eqCorpinfoId': widget.scanData['id'], - // 'eqParentId': widget.scanData['corpinfoId'], - }; - final result = await BasicInfoApi.getDeptTree(data); - if (result['success'] == true) { - final list = result['data'] ?? []; - if (list.length > 0) { + void chooseUnitHandle() { + final data = { + 'eqCorpinfoId': widget.scanData['id'], + // 'eqParentId': widget.scanData['corpinfoId'], + }; + showModalBottomSheet( + context: context, + isScrollControlled: true, + barrierColor: Colors.black54, + backgroundColor: Colors.transparent, + builder: + (_) => DepartmentPicker( + data: data, + onSelected: (id, name, data) async { setState(() { - _deptList = list[0]['childrenList'] ?? []; + pd['departmentId'] = data['id'] ?? ''; + pd['departmentName'] = data['name'] ?? ''; + pd['corpinfoId'] = data['corpinfoId'] ?? ''; + pd['corpinfoName'] = data['corpinfoName'] ?? ''; }); - } - } - } catch (e) {} + }, + ), + ).then((_) {}); } // 提交 @@ -109,7 +115,7 @@ class _OnboardingFullPageState extends State { ItemListWidget.singleLineTitleText( label: '企业名称', isEditable: false, - text: widget.scanData['corpName'] ?? '', + text: widget.scanData['corpName'] ?? widget.scanData['corpinfoName'] ?? '', ), const Divider(), ItemListWidget.selectableLineTitleTextRightButton( @@ -118,28 +124,8 @@ class _OnboardingFullPageState extends State { isEditable: true, text: pd['departmentName'] ?? '请选择', isRequired: true, - onTap: () async { - if (_deptList.isEmpty) { - ToastUtil.showNormal(context, '暂无部门信息'); - return; - } - final found = await BottomPicker.show( - context, - items: _deptList, - itemBuilder: - (i) => Text(i['name']!, textAlign: TextAlign.center), - initialIndex: 0, - ); - //FocusHelper.clearFocus(context); - - if (found != null) { - setState(() { - pd['departmentId'] = found['id']; - pd['departmentName'] = found['name']; - pd['corpinfoId'] = found['corpinfoId']; - pd['corpinfoName'] = found['corpinfoName']; - }); - } + onTap: () { + chooseUnitHandle(); }, ), const Divider(), diff --git a/lib/pages/user/firm_list_page.dart b/lib/pages/user/firm_list_page.dart index b531822..3ead651 100644 --- a/lib/pages/user/firm_list_page.dart +++ b/lib/pages/user/firm_list_page.dart @@ -458,7 +458,7 @@ class _FirmListPageState extends State { // 尽量让每个字母区域可点击,并根据启用状态改变样式 padding: const EdgeInsets.symmetric( vertical: 2, - horizontal: 4, + horizontal: 2, ), alignment: Alignment.center, child: diff --git a/lib/pages/user/full_userinfo_page.dart b/lib/pages/user/full_userinfo_page.dart index 6c0b5d8..9673373 100644 --- a/lib/pages/user/full_userinfo_page.dart +++ b/lib/pages/user/full_userinfo_page.dart @@ -181,6 +181,7 @@ class _FullUserinfoPageState extends State { await BasicInfoApi.updateUserInfo(pd, userIdCard).then((res) { LoadingDialogHelper.hide(); if (res['success']) { + SessionService.instance.name = pd['name']; ToastUtil.showNormal(context, '保存成功'); if (widget.isChooseFirm || _isChange) { diff --git a/lib/pages/user/login_page.dart b/lib/pages/user/login_page.dart index 8ced3a0..e314792 100644 --- a/lib/pages/user/login_page.dart +++ b/lib/pages/user/login_page.dart @@ -39,7 +39,7 @@ class _LoginPageState extends State { String _errorMessage = ''; bool _isLoading = false; bool _obscurePassword = true; - bool _agreed = false; + bool _agreed = true; String _captchaImageBase64 = ''; String _captchaIdentifier = ''; bool _rememberPassword = true; @@ -505,19 +505,20 @@ class _LoginPageState extends State { // 修改验证码图片构建方法 Widget _buildCaptchaImage() { if (_captchaImageBase64.isEmpty) { - return Container( - width: 100, - height: 40, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(4), - ), - child: const Center( - child: Text( - '加载中...', - style: TextStyle(color: Colors.grey, fontSize: 12), - ), - ), + return GestureDetector( + onTap: _getCaptcha, + child: Container( + width: 100, + height: 40, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + child: const Center( + child: Text('加载中...', + style: TextStyle(color: Colors.grey, fontSize: 12)), + ), + ) ); } diff --git a/lib/services/SessionService.dart b/lib/services/SessionService.dart index ea20513..7b3d10c 100644 --- a/lib/services/SessionService.dart +++ b/lib/services/SessionService.dart @@ -253,6 +253,7 @@ class SessionService { // 原有的认证相关字段(保留用于兼容性) String? token; + String? name; String? accessTicket; String? refreshTicket; String? expireIn; @@ -271,7 +272,6 @@ class SessionService { // 兼容性getter - 映射到新字段 String? get userId => userData?.userId; String? get accountId => userData?.id; - String? get name => userData?.name; String? get userName => userData?.username; String? get phone => userData?.phone; String? get email => userData?.email; @@ -293,6 +293,7 @@ class SessionService { Future clear({bool clearPrefs = true}) async { userData = null; token = null; + name = null; accessTicket = null; refreshTicket = null; expireIn = null; @@ -316,6 +317,7 @@ class SessionService { // 解析认证字段 s.token = json['token'] as String?; + s.name = json['name'] as String?; s.accessTicket = json['accessTicket'] as String?; s.refreshTicket = json['refreshTicket'] as String?; s.expireIn = json['expireIn'] as String?; @@ -330,6 +332,7 @@ class SessionService { return { 'userData': userData?.toJson(), 'token': token, + 'name': name, 'accessTicket': accessTicket, 'refreshTicket': refreshTicket, 'expireIn': expireIn, @@ -356,6 +359,7 @@ class SessionService { final i = SessionService.instance; i.userData = newSession.userData; i.token = newSession.token; + i.name = newSession.name; i.accessTicket = newSession.accessTicket; i.refreshTicket = newSession.refreshTicket; i.expireIn = newSession.expireIn; @@ -391,6 +395,8 @@ class SessionService { // ---------- convenience setters ---------- void setToken(String t) => token = t; + void setName(String t) => name = t; + void setLoginPhone(String phone) => loginPhone = phone; void setLoginPass(String pass) => loginPass = pass; diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 60505ad..0464b40 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -83,6 +83,7 @@ class AuthService { }; } } else { + StorageService.instance.remove('key.saveJoinFirmInfo'); return { 'isChooseFirm': false, 'isInfoComplete': isInfoComplete, diff --git a/lib/tools/smart_image.dart b/lib/tools/smart_image.dart new file mode 100644 index 0000000..c2983f3 --- /dev/null +++ b/lib/tools/smart_image.dart @@ -0,0 +1,179 @@ +// lib/widgets/smart_image.dart +import 'package:flutter/material.dart'; + +/// SmartAssetImage +/// - 自动计算目标像素宽度(devicePixelRatio * context.width) +/// - 使用 ResizeImage 限制解码尺寸以降低首次解码开销 +/// - 预缓存(precacheImage),并提供占位色/小占位Widget +/// - gaplessPlayback 避免重建时闪烁 +class SmartAssetImage extends StatefulWidget { + final String assetPath; + final double? height; + final double? width; + final BoxFit fit; + final Color placeholderColor; + final Duration precacheTimeout; + + const SmartAssetImage({ + Key? key, + required this.assetPath, + this.height, + this.width, + this.fit = BoxFit.cover, + this.placeholderColor = const Color(0xFFE8F4FD), + this.precacheTimeout = const Duration(milliseconds: 800), + }) : super(key: key); + + @override + State createState() => _SmartAssetImageState(); +} + +class _SmartAssetImageState extends State { + ImageProvider? _provider; + bool _done = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _prepareImage(); + } + + Future _prepareImage() async { + // 避免重复处理 + if (_done) return; + + try { + final mq = MediaQuery.of(context); + final logicalWidth = widget.width ?? mq.size.width; + final devicePixelRatio = mq.devicePixelRatio; + final int targetWidth = (logicalWidth * devicePixelRatio).toInt(); + + // 使用 ResizeImage 限制解码到合适的像素宽度 + final provider = ResizeImage( + AssetImage(widget.assetPath), + width: targetWidth, + ); + + // attempt to precache but don't block too long + final precacheFuture = precacheImage(provider, context); + // optional: bound the wait time to prevent waiting forever + await precacheFuture + .timeout(widget.precacheTimeout, onTimeout: () => null); + + if (mounted) { + setState(() { + _provider = provider; + _done = true; + }); + } + } catch (e) { + // 如果预缓存失败,仍然回退到常规 AssetImage + if (mounted) { + setState(() { + _provider = AssetImage(widget.assetPath); + _done = true; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final placeholder = Container( + width: widget.width ?? double.infinity, + height: widget.height, + color: widget.placeholderColor, + ); + + if (_provider == null) { + return placeholder; + } + + return Image( + image: _provider!, + width: widget.width ?? double.infinity, + height: widget.height, + fit: widget.fit, + gaplessPlayback: true, + ); + } +} + +/// SmartNetworkImage (无第三方包版本) +/// - 先显示 placeholder / local placeholder +/// - 再通过 precacheImage 加载 NetworkImage +class SmartNetworkImage extends StatefulWidget { + final String url; + final double? height; + final double? width; + final BoxFit fit; + final Widget? placeholder; + final Duration precacheTimeout; + + const SmartNetworkImage({ + Key? key, + required this.url, + this.height, + this.width, + this.fit = BoxFit.cover, + this.placeholder, + this.precacheTimeout = const Duration(milliseconds: 800), + }) : super(key: key); + + @override + State createState() => _SmartNetworkImageState(); +} + +class _SmartNetworkImageState extends State { + ImageProvider? _provider; + bool _done = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _prepare(); + } + + Future _prepare() async { + if (_done) return; + try { + final provider = NetworkImage(widget.url); + await precacheImage(provider, context).timeout(widget.precacheTimeout, onTimeout: () => null); + if (mounted) { + setState(() { + _provider = provider; + _done = true; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _provider = NetworkImage(widget.url); // fallback + _done = true; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final placeholder = widget.placeholder ?? + Container( + width: widget.width ?? double.infinity, + height: widget.height, + color: const Color(0xFFE8F4FD), + ); + + if (_provider == null) { + return placeholder; + } + + return Image( + image: _provider!, + width: widget.width ?? double.infinity, + height: widget.height, + fit: widget.fit, + gaplessPlayback: true, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 3b01db7..2623329 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -154,6 +154,7 @@ flutter: - assets/js/ - assets/map/ - assets/tabbar/ + - assets/study/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg