commit d39a355ede3abfbc31f32824e0d96ebab2951d41 Author: hs <873121290@qq.com> Date: Fri Dec 12 09:11:30 2025 +0800 秦港相关方 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..814bc41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json +/lib/git-cheatsheet.md +# Android Studio will place build artifacts here +/android/app/debug +/android/app/release \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..e2ff7d7 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d7b523b356d15fb81e7d340bbe52b47f93937323" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + - platform: web + create_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + base_revision: d7b523b356d15fb81e7d340bbe52b47f93937323 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.tgitconfig b/.tgitconfig new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..62e7dd4 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# flutter_integrated_whb + +危化项目. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..5c7a25e --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + camel_case_types: false + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..3085927 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,66 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} +// 🔐 加载 key.properties +val keystoreProperties = Properties() +val keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(keystorePropertiesFile.inputStream()) +} + +android { + namespace = "com.company.myapp2" + compileSdk = flutter.compileSdkVersion + ndkVersion = "28.1.13356709" + + // 使用 JDK 17 / JavaVersion.VERSION_17(推荐与 AGP 8.9.x 兼容) + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + // jvmTarget 要与 Java 版本一致 + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + applicationId = "com.company.myapp2" + minSdk = 24 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + create("release") { + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + } + // debug signingConfig 通常存在(Android Gradle 会有默认 debug 签名) + // 如果你自定义 debug 签名,则可以在这里创建 + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + isMinifyEnabled = false + isShrinkResources = false + } + debug { + // 使用默认 debug 签名(或你自定义的 debug) + // signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..349325a --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/company/myapp2/MainActivity.kt b/android/app/src/main/kotlin/com/company/myapp2/MainActivity.kt new file mode 100644 index 0000000..59c30c2 --- /dev/null +++ b/android/app/src/main/kotlin/com/company/myapp2/MainActivity.kt @@ -0,0 +1,123 @@ +package com.company.myapp2 + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import androidx.core.content.FileProvider +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.io.File + +class MainActivity: FlutterActivity() { + private val CHANNEL = "app.install" + private val REQ_INSTALL_UNKNOWN = 9999 + + // 暂存安装请求(仅在跳转设置并等待返回时使用) + private var pendingApkPath: String? = null + private var pendingResult: MethodChannel.Result? = null + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "installApk" -> { + val path = call.argument("path") + if (path == null) { + result.error("NO_PATH", "no path provided", null) + return@setMethodCallHandler + } + handleInstallRequest(path, result) + } + else -> result.notImplemented() + } + } + } + + private fun handleInstallRequest(path: String, result: MethodChannel.Result) { + val file = File(path) + if (!file.exists()) { + result.error("NO_FILE", "file not exist", null) + return + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 8.0+ 需要 app 级别未知来源授权 + if (!packageManager.canRequestPackageInstalls()) { + // 存储请求信息以便用户返回后继续 + pendingApkPath = path + pendingResult = result + + val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:$packageName")) + // 使用 startActivityForResult 以便用户返回后可以继续安装 + startActivityForResult(intent, REQ_INSTALL_UNKNOWN) + return + } + } + // 已有授权 或 非 8.0+:直接安装 + installApkInternal(path, result) + } + + // 真正执行安装的函数(假定有权限) + private fun installApkInternal(path: String, result: MethodChannel.Result) { + val file = File(path) + if (!file.exists()) { + result.error("NO_FILE", "file not exist", null) + return + } + + try { + val apkUri: Uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(apkUri, "application/vnd.android.package-archive") + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivity(intent) + result.success(true) + } catch (e: Exception) { + result.error("INSTALL_FAILED", e.message, null) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_INSTALL_UNKNOWN) { + // 用户从系统设置页返回后,检查是否已授权 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (packageManager.canRequestPackageInstalls()) { + // 授权已开:继续安装 + val path = pendingApkPath + val res = pendingResult + // 清理 pending 状态 + pendingApkPath = null + pendingResult = null + if (path != null && res != null) { + installApkInternal(path, res) + } else { + // 安全兜底:若没有 pending 数据,通知 caller 重新触发 + res?.error("NO_PENDING", "no pending install info", null) + } + } else { + // 用户仍未授权 + pendingApkPath = null + pendingResult?.error("NEED_INSTALL_PERMISSION", "user did not allow install unknown apps", null) + pendingResult = null + } + } else { + // API < 26:尝试直接安装一次作为尝试(某些 ROM 无法精准判断) + val path = pendingApkPath + val res = pendingResult + pendingApkPath = null + pendingResult = null + if (path != null && res != null) { + installApkInternal(path, res) + } else { + res?.error("NO_PENDING", "no pending install info", null) + } + } + } + } +} diff --git a/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt b/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt new file mode 100644 index 0000000..7433425 --- /dev/null +++ b/android/app/src/main/kotlin/com/zhuoyun/qhdprevention/qhd_prevention/MyApplication.kt @@ -0,0 +1,38 @@ +package com.company.myapp2 + +import android.app.Application +import android.content.Context +import android.util.Log + +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + // 通过反射调用 SDKInitializer,避免在编译期要求依赖存在 + try { + val sdkClass = Class.forName("com.baidu.mapapi.SDKInitializer") + // setAgreePrivacy(boolean) - 有些 SDK 要求先同意隐私 + try { + val setAgree = sdkClass.getMethod("setAgreePrivacy", java.lang.Boolean::class.javaPrimitiveType) + setAgree.invoke(null, true) + Log.i("MyApplication", "SDKInitializer.setAgreePrivacy invoked via reflection") + } catch (t: Throwable) { + Log.w("MyApplication", "setAgreePrivacy not available or failed: ${t.message}") + } + + // initialize(Context) + try { + val initMethod = sdkClass.getMethod("initialize", Context::class.java) + initMethod.invoke(null, this) + Log.i("MyApplication", "SDKInitializer.initialize invoked via reflection") + } catch (t: Throwable) { + Log.w("MyApplication", "initialize(Context) not available or failed: ${t.message}") + } + } catch (e: ClassNotFoundException) { + // 运行时没有找到该类(可能 plugin 未包含 SDK),记录日志但不崩溃 + Log.w("MyApplication", "Baidu SDKInitializer class not found at runtime: ${e.message}") + } catch (e: Throwable) { + Log.e("MyApplication", "Unexpected error while initializing Baidu SDK via reflection", e) + } + } +} diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..4ebb677 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f88598c --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..4ebb677 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..f88598c --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..5337d5a Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..8c85bf1 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..e37e85c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..342548c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..7e763e2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..5fef228 --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..dbc9ea9 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..d0a68e9 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..35f833e --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 秦港相关方 + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..0d1fa8f --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..80def29 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..5e5704e --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..24863d2 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cf3c25b --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +#distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..9fcdbe2 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,37 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + resolutionStrategy { + eachPlugin { + val pluginId = requested.id.id + if (pluginId == "org.jetbrains.kotlin.android" || pluginId == "org.jetbrains.kotlin.jvm") { + // 与 build.gradle.kts 中 kotlin 插件版本保持一致 + useVersion("2.2.20") + } + } + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + // 与 android/build.gradle.kts 中使用的 AGP 版本保持一致 + id("com.android.application") version "8.9.1" apply false + // 与 build.gradle.kts 中 kotlin 插件版本保持一致 + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/assets/icon-apps/home_ico10.png b/assets/icon-apps/home_ico10.png new file mode 100644 index 0000000..3c2abc3 Binary files /dev/null and b/assets/icon-apps/home_ico10.png differ diff --git a/assets/icon-apps/home_ico11.png b/assets/icon-apps/home_ico11.png new file mode 100644 index 0000000..8ed4996 Binary files /dev/null and b/assets/icon-apps/home_ico11.png differ diff --git a/assets/icon-apps/home_ico4.png b/assets/icon-apps/home_ico4.png new file mode 100644 index 0000000..11b0496 Binary files /dev/null and b/assets/icon-apps/home_ico4.png differ diff --git a/assets/icon-apps/home_ico5.png b/assets/icon-apps/home_ico5.png new file mode 100644 index 0000000..b54ce14 Binary files /dev/null and b/assets/icon-apps/home_ico5.png differ diff --git a/assets/icon-apps/home_ico6.png b/assets/icon-apps/home_ico6.png new file mode 100644 index 0000000..501ba27 Binary files /dev/null and b/assets/icon-apps/home_ico6.png differ diff --git a/assets/icon-apps/home_ico7.png b/assets/icon-apps/home_ico7.png new file mode 100644 index 0000000..51b7677 Binary files /dev/null and b/assets/icon-apps/home_ico7.png differ diff --git a/assets/icon-apps/home_ico8.png b/assets/icon-apps/home_ico8.png new file mode 100644 index 0000000..802c196 Binary files /dev/null and b/assets/icon-apps/home_ico8.png differ diff --git a/assets/icon-apps/home_ico9.png b/assets/icon-apps/home_ico9.png new file mode 100644 index 0000000..9248c16 Binary files /dev/null and b/assets/icon-apps/home_ico9.png differ diff --git a/assets/icon-apps/home_saoyisao.png b/assets/icon-apps/home_saoyisao.png new file mode 100644 index 0000000..9bb9b4d Binary files /dev/null and b/assets/icon-apps/home_saoyisao.png differ diff --git a/assets/images/banner.png b/assets/images/banner.png new file mode 100644 index 0000000..b45ab94 Binary files /dev/null and b/assets/images/banner.png differ diff --git a/assets/images/bg1.png b/assets/images/bg1.png new file mode 100644 index 0000000..0021e04 Binary files /dev/null and b/assets/images/bg1.png differ diff --git a/assets/images/bg2.png b/assets/images/bg2.png new file mode 100644 index 0000000..a7b3310 Binary files /dev/null and b/assets/images/bg2.png differ diff --git a/assets/images/g_logo.png b/assets/images/g_logo.png new file mode 100644 index 0000000..e538a42 Binary files /dev/null and b/assets/images/g_logo.png differ diff --git a/assets/images/home_banner.png b/assets/images/home_banner.png new file mode 100644 index 0000000..ab6fe67 Binary files /dev/null and b/assets/images/home_banner.png differ diff --git a/assets/images/ico1.png b/assets/images/ico1.png new file mode 100644 index 0000000..db8f3bc Binary files /dev/null and b/assets/images/ico1.png differ diff --git a/assets/images/ico10.png b/assets/images/ico10.png new file mode 100644 index 0000000..f552067 Binary files /dev/null and b/assets/images/ico10.png differ diff --git a/assets/images/ico11.png b/assets/images/ico11.png new file mode 100644 index 0000000..e6866ce Binary files /dev/null and b/assets/images/ico11.png differ diff --git a/assets/images/ico12.png b/assets/images/ico12.png new file mode 100644 index 0000000..33d14b2 Binary files /dev/null and b/assets/images/ico12.png differ diff --git a/assets/images/ico13.png b/assets/images/ico13.png new file mode 100644 index 0000000..6579a7e Binary files /dev/null and b/assets/images/ico13.png differ diff --git a/assets/images/ico14.png b/assets/images/ico14.png new file mode 100644 index 0000000..9429020 Binary files /dev/null and b/assets/images/ico14.png differ diff --git a/assets/images/ico15.png b/assets/images/ico15.png new file mode 100644 index 0000000..338ad0a Binary files /dev/null and b/assets/images/ico15.png differ diff --git a/assets/images/ico16.png b/assets/images/ico16.png new file mode 100644 index 0000000..176801c Binary files /dev/null and b/assets/images/ico16.png differ diff --git a/assets/images/ico2.png b/assets/images/ico2.png new file mode 100644 index 0000000..15a5e92 Binary files /dev/null and b/assets/images/ico2.png differ diff --git a/assets/images/ico3.png b/assets/images/ico3.png new file mode 100644 index 0000000..7b9e46c Binary files /dev/null and b/assets/images/ico3.png differ diff --git a/assets/images/ico4.png b/assets/images/ico4.png new file mode 100644 index 0000000..15258f7 Binary files /dev/null and b/assets/images/ico4.png differ diff --git a/assets/images/ico5.png b/assets/images/ico5.png new file mode 100644 index 0000000..8860b79 Binary files /dev/null and b/assets/images/ico5.png differ diff --git a/assets/images/ico6.png b/assets/images/ico6.png new file mode 100644 index 0000000..525779c Binary files /dev/null and b/assets/images/ico6.png differ diff --git a/assets/images/ico7.png b/assets/images/ico7.png new file mode 100644 index 0000000..6fe5208 Binary files /dev/null and b/assets/images/ico7.png differ diff --git a/assets/images/ico8.png b/assets/images/ico8.png new file mode 100644 index 0000000..35fbaec Binary files /dev/null and b/assets/images/ico8.png differ diff --git a/assets/images/ico9.png b/assets/images/ico9.png new file mode 100644 index 0000000..1bfa162 Binary files /dev/null and b/assets/images/ico9.png differ diff --git a/assets/images/img1.png b/assets/images/img1.png new file mode 100644 index 0000000..dfeea15 Binary files /dev/null and b/assets/images/img1.png differ diff --git a/assets/images/img2.png b/assets/images/img2.png new file mode 100644 index 0000000..e39de8d Binary files /dev/null and b/assets/images/img2.png differ diff --git a/assets/images/loginbg.png b/assets/images/loginbg.png new file mode 100644 index 0000000..d2ab811 Binary files /dev/null and b/assets/images/loginbg.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..64c5599 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/lun.jpg b/assets/images/lun.jpg new file mode 100644 index 0000000..bd769ef Binary files /dev/null and b/assets/images/lun.jpg differ diff --git a/assets/images/more.png b/assets/images/more.png new file mode 100644 index 0000000..56d9ca7 Binary files /dev/null and b/assets/images/more.png differ diff --git a/assets/images/my_bg.png b/assets/images/my_bg.png new file mode 100644 index 0000000..600c5a7 Binary files /dev/null and b/assets/images/my_bg.png differ diff --git a/assets/images/null.png b/assets/images/null.png new file mode 100644 index 0000000..7df785c Binary files /dev/null and b/assets/images/null.png differ diff --git a/assets/images/search.png b/assets/images/search.png new file mode 100644 index 0000000..1aec6e2 Binary files /dev/null and b/assets/images/search.png differ diff --git a/assets/images/unit_banner.jpg b/assets/images/unit_banner.jpg new file mode 100644 index 0000000..bb3daae Binary files /dev/null and b/assets/images/unit_banner.jpg differ diff --git a/assets/images/unit_ico1.png b/assets/images/unit_ico1.png new file mode 100644 index 0000000..eb66ae6 Binary files /dev/null and b/assets/images/unit_ico1.png differ diff --git a/assets/images/unit_ico2.png b/assets/images/unit_ico2.png new file mode 100644 index 0000000..acdcf24 Binary files /dev/null and b/assets/images/unit_ico2.png differ diff --git a/assets/js/jsencrypt.min.js b/assets/js/jsencrypt.min.js new file mode 100644 index 0000000..174916b --- /dev/null +++ b/assets/js/jsencrypt.min.js @@ -0,0 +1,2 @@ +/*! For license information please see jsencrypt.min.js.LICENSE.txt */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.JSEncrypt=e():t.JSEncrypt=e()}(window,(()=>(()=>{var t={155:t=>{var e,i,r=t.exports={};function n(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function o(t){if(e===setTimeout)return setTimeout(t,0);if((e===n||!e)&&setTimeout)return e=setTimeout,setTimeout(t,0);try{return e(t,0)}catch(i){try{return e.call(null,t,0)}catch(i){return e.call(this,t,0)}}}!function(){try{e="function"==typeof setTimeout?setTimeout:n}catch(t){e=n}try{i="function"==typeof clearTimeout?clearTimeout:s}catch(t){i=s}}();var h,a=[],u=!1,c=-1;function f(){u&&h&&(u=!1,h.length?a=h.concat(a):c=-1,a.length&&l())}function l(){if(!u){var t=o(f);u=!0;for(var e=a.length;e;){for(h=a,a=[];++c1)for(var i=1;i{for(var r in e)i.o(e,r)&&!i.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var r={};return(()=>{"use strict";function t(t){return"0123456789abcdefghijklmnopqrstuvwxyz".charAt(t)}function e(t,e){return t&e}function n(t,e){return t|e}function s(t,e){return t^e}function o(t,e){return t&~e}function h(t){if(0==t)return-1;var e=0;return 0==(65535&t)&&(t>>=16,e+=16),0==(255&t)&&(t>>=8,e+=8),0==(15&t)&&(t>>=4,e+=4),0==(3&t)&&(t>>=2,e+=2),0==(1&t)&&++e,e}function a(t){for(var e=0;0!=t;)t&=t-1,++e;return e}i.d(r,{default:()=>ot});var u,c="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function f(t){var e,i,r="";for(e=0;e+3<=t.length;e+=3)i=parseInt(t.substring(e,e+3),16),r+=c.charAt(i>>6)+c.charAt(63&i);for(e+1==t.length?(i=parseInt(t.substring(e,e+1),16),r+=c.charAt(i<<2)):e+2==t.length&&(i=parseInt(t.substring(e,e+2),16),r+=c.charAt(i>>2)+c.charAt((3&i)<<4));(3&r.length)>0;)r+="=";return r}function l(e){var i,r="",n=0,s=0;for(i=0;i>2),s=3&o,n=1):1==n?(r+=t(s<<2|o>>4),s=15&o,n=2):2==n?(r+=t(s),r+=t(o>>2),s=3&o,n=3):(r+=t(s<<2|o>>4),r+=t(15&o),n=0))}return 1==n&&(r+=t(s<<2)),r}var p,g={decode:function(t){var e;if(void 0===p){var i="= \f\n\r\t \u2028\u2029";for(p=Object.create(null),e=0;e<64;++e)p["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(e)]=e;for(p["-"]=62,p._=63,e=0;e=4?(r[r.length]=n>>16,r[r.length]=n>>8&255,r[r.length]=255&n,n=0,s=0):n<<=6}}switch(s){case 1:throw new Error("Base64 encoding incomplete: at least 2 bits missing");case 2:r[r.length]=n>>10;break;case 3:r[r.length]=n>>16,r[r.length]=n>>8&255}return r},re:/-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/,unarmor:function(t){var e=g.re.exec(t);if(e)if(e[1])t=e[1];else{if(!e[2])throw new Error("RegExp out of sync");t=e[2]}return g.decode(t)}},d=1e13,v=function(){function t(t){this.buf=[+t||0]}return t.prototype.mulAdd=function(t,e){var i,r,n=this.buf,s=n.length;for(i=0;i0&&(n[i]=e)},t.prototype.sub=function(t){var e,i,r=this.buf,n=r.length;for(e=0;e=0;--r)i+=(d+e[r]).toString().substring(1);return i},t.prototype.valueOf=function(){for(var t=this.buf,e=0,i=t.length-1;i>=0;--i)e=e*d+t[i];return e},t.prototype.simplify=function(){var t=this.buf;return 1==t.length?t[0]:this},t}(),m=/^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/,y=/^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;function b(t,e){return t.length>e&&(t=t.substring(0,e)+"…"),t}var T,S=function(){function t(e,i){this.hexDigits="0123456789ABCDEF",e instanceof t?(this.enc=e.enc,this.pos=e.pos):(this.enc=e,this.pos=i)}return t.prototype.get=function(t){if(void 0===t&&(t=this.pos++),t>=this.enc.length)throw new Error("Requesting byte offset ".concat(t," on a stream of length ").concat(this.enc.length));return"string"==typeof this.enc?this.enc.charCodeAt(t):this.enc[t]},t.prototype.hexByte=function(t){return this.hexDigits.charAt(t>>4&15)+this.hexDigits.charAt(15&t)},t.prototype.hexDump=function(t,e,i){for(var r="",n=t;n176)return!1}return!0},t.prototype.parseStringISO=function(t,e){for(var i="",r=t;r191&&n<224?String.fromCharCode((31&n)<<6|63&this.get(r++)):String.fromCharCode((15&n)<<12|(63&this.get(r++))<<6|63&this.get(r++))}return i},t.prototype.parseStringBMP=function(t,e){for(var i,r,n="",s=t;s127,s=n?255:0,o="";r==s&&++t4){for(o=r,i<<=3;0==(128&(+o^s));)o=+o<<1,--i;o="("+i+" bit)\n"}n&&(r-=256);for(var h=new v(r),a=t+1;a=a;--u)s+=h>>u&1?"1":"0";if(s.length>i)return n+b(s,i)}return n+s},t.prototype.parseOctetString=function(t,e,i){if(this.isASCII(t,e))return b(this.parseStringISO(t,e),i);var r=e-t,n="("+r+" byte)\n";r>(i/=2)&&(e=t+i);for(var s=t;si&&(n+="…"),n},t.prototype.parseOID=function(t,e,i){for(var r="",n=new v,s=0,o=t;oi)return b(r,i);n=new v,s=0}}return s>0&&(r+=".incomplete"),r},t}(),E=function(){function t(t,e,i,r,n){if(!(r instanceof w))throw new Error("Invalid tag value.");this.stream=t,this.header=e,this.length=i,this.tag=r,this.sub=n}return t.prototype.typeName=function(){switch(this.tag.tagClass){case 0:switch(this.tag.tagNumber){case 0:return"EOC";case 1:return"BOOLEAN";case 2:return"INTEGER";case 3:return"BIT_STRING";case 4:return"OCTET_STRING";case 5:return"NULL";case 6:return"OBJECT_IDENTIFIER";case 7:return"ObjectDescriptor";case 8:return"EXTERNAL";case 9:return"REAL";case 10:return"ENUMERATED";case 11:return"EMBEDDED_PDV";case 12:return"UTF8String";case 16:return"SEQUENCE";case 17:return"SET";case 18:return"NumericString";case 19:return"PrintableString";case 20:return"TeletexString";case 21:return"VideotexString";case 22:return"IA5String";case 23:return"UTCTime";case 24:return"GeneralizedTime";case 25:return"GraphicString";case 26:return"VisibleString";case 27:return"GeneralString";case 28:return"UniversalString";case 30:return"BMPString"}return"Universal_"+this.tag.tagNumber.toString();case 1:return"Application_"+this.tag.tagNumber.toString();case 2:return"["+this.tag.tagNumber.toString()+"]";case 3:return"Private_"+this.tag.tagNumber.toString()}},t.prototype.content=function(t){if(void 0===this.tag)return null;void 0===t&&(t=1/0);var e=this.posContent(),i=Math.abs(this.length);if(!this.tag.isUniversal())return null!==this.sub?"("+this.sub.length+" elem)":this.stream.parseOctetString(e,e+i,t);switch(this.tag.tagNumber){case 1:return 0===this.stream.get(e)?"false":"true";case 2:return this.stream.parseInteger(e,e+i);case 3:return this.sub?"("+this.sub.length+" elem)":this.stream.parseBitString(e,e+i,t);case 4:return this.sub?"("+this.sub.length+" elem)":this.stream.parseOctetString(e,e+i,t);case 6:return this.stream.parseOID(e,e+i,t);case 16:case 17:return null!==this.sub?"("+this.sub.length+" elem)":"(no elem)";case 12:return b(this.stream.parseStringUTF(e,e+i),t);case 18:case 19:case 20:case 21:case 22:case 26:return b(this.stream.parseStringISO(e,e+i),t);case 30:return b(this.stream.parseStringBMP(e,e+i),t);case 23:case 24:return this.stream.parseTime(e,e+i,23==this.tag.tagNumber)}return null},t.prototype.toString=function(){return this.typeName()+"@"+this.stream.pos+"[header:"+this.header+",length:"+this.length+",sub:"+(null===this.sub?"null":this.sub.length)+"]"},t.prototype.toPrettyString=function(t){void 0===t&&(t="");var e=t+this.typeName()+" @"+this.stream.pos;if(this.length>=0&&(e+="+"),e+=this.length,this.tag.tagConstructed?e+=" (constructed)":!this.tag.isUniversal()||3!=this.tag.tagNumber&&4!=this.tag.tagNumber||null===this.sub||(e+=" (encapsulates)"),e+="\n",null!==this.sub){t+=" ";for(var i=0,r=this.sub.length;i6)throw new Error("Length over 48 bits not supported at position "+(t.pos-1));if(0===i)return null;e=0;for(var r=0;r>6,this.tagConstructed=0!=(32&e),this.tagNumber=31&e,31==this.tagNumber){var i=new v;do{e=t.get(),i.mulAdd(128,127&e)}while(128&e);this.tagNumber=i.simplify()}}return t.prototype.isUniversal=function(){return 0===this.tagClass},t.prototype.isEOC=function(){return 0===this.tagClass&&0===this.tagNumber},t}(),D=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997],x=(1<<26)/D[D.length-1],R=function(){function i(t,e,i){null!=t&&("number"==typeof t?this.fromNumber(t,e,i):null==e&&"string"!=typeof t?this.fromString(t,256):this.fromString(t,e))}return i.prototype.toString=function(e){if(this.s<0)return"-"+this.negate().toString(e);var i;if(16==e)i=4;else if(8==e)i=3;else if(2==e)i=1;else if(32==e)i=5;else{if(4!=e)return this.toRadix(e);i=2}var r,n=(1<0)for(a>a)>0&&(s=!0,o=t(r));h>=0;)a>(a+=this.DB-i)):(r=this[h]>>(a-=i)&n,a<=0&&(a+=this.DB,--h)),r>0&&(s=!0),s&&(o+=t(r));return s?o:"0"},i.prototype.negate=function(){var t=I();return i.ZERO.subTo(this,t),t},i.prototype.abs=function(){return this.s<0?this.negate():this},i.prototype.compareTo=function(t){var e=this.s-t.s;if(0!=e)return e;var i=this.t;if(0!=(e=i-t.t))return this.s<0?-e:e;for(;--i>=0;)if(0!=(e=this[i]-t[i]))return e;return 0},i.prototype.bitLength=function(){return this.t<=0?0:this.DB*(this.t-1)+C(this[this.t-1]^this.s&this.DM)},i.prototype.mod=function(t){var e=I();return this.abs().divRemTo(t,null,e),this.s<0&&e.compareTo(i.ZERO)>0&&t.subTo(e,e),e},i.prototype.modPowInt=function(t,e){var i;return i=t<256||e.isEven()?new O(e):new A(e),this.exp(t,i)},i.prototype.clone=function(){var t=I();return this.copyTo(t),t},i.prototype.intValue=function(){if(this.s<0){if(1==this.t)return this[0]-this.DV;if(0==this.t)return-1}else{if(1==this.t)return this[0];if(0==this.t)return 0}return(this[1]&(1<<32-this.DB)-1)<>24},i.prototype.shortValue=function(){return 0==this.t?this.s:this[0]<<16>>16},i.prototype.signum=function(){return this.s<0?-1:this.t<=0||1==this.t&&this[0]<=0?0:1},i.prototype.toByteArray=function(){var t=this.t,e=[];e[0]=this.s;var i,r=this.DB-t*this.DB%8,n=0;if(t-- >0)for(r>r)!=(this.s&this.DM)>>r&&(e[n++]=i|this.s<=0;)r<8?(i=(this[t]&(1<>(r+=this.DB-8)):(i=this[t]>>(r-=8)&255,r<=0&&(r+=this.DB,--t)),0!=(128&i)&&(i|=-256),0==n&&(128&this.s)!=(128&i)&&++n,(n>0||i!=this.s)&&(e[n++]=i);return e},i.prototype.equals=function(t){return 0==this.compareTo(t)},i.prototype.min=function(t){return this.compareTo(t)<0?this:t},i.prototype.max=function(t){return this.compareTo(t)>0?this:t},i.prototype.and=function(t){var i=I();return this.bitwiseTo(t,e,i),i},i.prototype.or=function(t){var e=I();return this.bitwiseTo(t,n,e),e},i.prototype.xor=function(t){var e=I();return this.bitwiseTo(t,s,e),e},i.prototype.andNot=function(t){var e=I();return this.bitwiseTo(t,o,e),e},i.prototype.not=function(){for(var t=I(),e=0;e=this.t?0!=this.s:0!=(this[e]&1<1){var c=I();for(r.sqrTo(o[1],c);h<=u;)o[h]=I(),r.mulTo(c,o[h-2],o[h]),h+=2}var f,l,p=t.t-1,g=!0,d=I();for(n=C(t[p])-1;p>=0;){for(n>=a?f=t[p]>>n-a&u:(f=(t[p]&(1<0&&(f|=t[p-1]>>this.DB+n-a)),h=i;0==(1&f);)f>>=1,--h;if((n-=h)<0&&(n+=this.DB,--p),g)o[f].copyTo(s),g=!1;else{for(;h>1;)r.sqrTo(s,d),r.sqrTo(d,s),h-=2;h>0?r.sqrTo(s,d):(l=s,s=d,d=l),r.mulTo(d,o[f],s)}for(;p>=0&&0==(t[p]&1<=0?(r.subTo(n,r),e&&s.subTo(h,s),o.subTo(a,o)):(n.subTo(r,n),e&&h.subTo(s,h),a.subTo(o,a))}return 0!=n.compareTo(i.ONE)?i.ZERO:a.compareTo(t)>=0?a.subtract(t):a.signum()<0?(a.addTo(t,a),a.signum()<0?a.add(t):a):a},i.prototype.pow=function(t){return this.exp(t,new B)},i.prototype.gcd=function(t){var e=this.s<0?this.negate():this.clone(),i=t.s<0?t.negate():t.clone();if(e.compareTo(i)<0){var r=e;e=i,i=r}var n=e.getLowestSetBit(),s=i.getLowestSetBit();if(s<0)return e;for(n0&&(e.rShiftTo(s,e),i.rShiftTo(s,i));e.signum()>0;)(n=e.getLowestSetBit())>0&&e.rShiftTo(n,e),(n=i.getLowestSetBit())>0&&i.rShiftTo(n,i),e.compareTo(i)>=0?(e.subTo(i,e),e.rShiftTo(1,e)):(i.subTo(e,i),i.rShiftTo(1,i));return s>0&&i.lShiftTo(s,i),i},i.prototype.isProbablePrime=function(t){var e,i=this.abs();if(1==i.t&&i[0]<=D[D.length-1]){for(e=0;e=0;--e)t[e]=this[e];t.t=this.t,t.s=this.s},i.prototype.fromInt=function(t){this.t=1,this.s=t<0?-1:0,t>0?this[0]=t:t<-1?this[0]=t+this.DV:this.t=0},i.prototype.fromString=function(t,e){var r;if(16==e)r=4;else if(8==e)r=3;else if(256==e)r=8;else if(2==e)r=1;else if(32==e)r=5;else{if(4!=e)return void this.fromRadix(t,e);r=2}this.t=0,this.s=0;for(var n=t.length,s=!1,o=0;--n>=0;){var h=8==r?255&+t[n]:q(t,n);h<0?"-"==t.charAt(n)&&(s=!0):(s=!1,0==o?this[this.t++]=h:o+r>this.DB?(this[this.t-1]|=(h&(1<>this.DB-o):this[this.t-1]|=h<=this.DB&&(o-=this.DB))}8==r&&0!=(128&+t[0])&&(this.s=-1,o>0&&(this[this.t-1]|=(1<0&&this[this.t-1]==t;)--this.t},i.prototype.dlShiftTo=function(t,e){var i;for(i=this.t-1;i>=0;--i)e[i+t]=this[i];for(i=t-1;i>=0;--i)e[i]=0;e.t=this.t+t,e.s=this.s},i.prototype.drShiftTo=function(t,e){for(var i=t;i=0;--h)e[h+s+1]=this[h]>>r|o,o=(this[h]&n)<=0;--h)e[h]=0;e[s]=o,e.t=this.t+s+1,e.s=this.s,e.clamp()},i.prototype.rShiftTo=function(t,e){e.s=this.s;var i=Math.floor(t/this.DB);if(i>=this.t)e.t=0;else{var r=t%this.DB,n=this.DB-r,s=(1<>r;for(var o=i+1;o>r;r>0&&(e[this.t-i-1]|=(this.s&s)<>=this.DB;if(t.t>=this.DB;r+=this.s}else{for(r+=this.s;i>=this.DB;r-=t.s}e.s=r<0?-1:0,r<-1?e[i++]=this.DV+r:r>0&&(e[i++]=r),e.t=i,e.clamp()},i.prototype.multiplyTo=function(t,e){var r=this.abs(),n=t.abs(),s=r.t;for(e.t=s+n.t;--s>=0;)e[s]=0;for(s=0;s=0;)t[i]=0;for(i=0;i=e.DV&&(t[i+e.t]-=e.DV,t[i+e.t+1]=1)}t.t>0&&(t[t.t-1]+=e.am(i,e[i],t,2*i,0,1)),t.s=0,t.clamp()},i.prototype.divRemTo=function(t,e,r){var n=t.abs();if(!(n.t<=0)){var s=this.abs();if(s.t0?(n.lShiftTo(u,o),s.lShiftTo(u,r)):(n.copyTo(o),s.copyTo(r));var c=o.t,f=o[c-1];if(0!=f){var l=f*(1<1?o[c-2]>>this.F2:0),p=this.FV/l,g=(1<=0&&(r[r.t++]=1,r.subTo(y,r)),i.ONE.dlShiftTo(c,y),y.subTo(o,o);o.t=0;){var b=r[--v]==f?this.DM:Math.floor(r[v]*p+(r[v-1]+d)*g);if((r[v]+=o.am(0,b,r,m,0,c))0&&r.rShiftTo(u,r),h<0&&i.ZERO.subTo(r,r)}}},i.prototype.invDigit=function(){if(this.t<1)return 0;var t=this[0];if(0==(1&t))return 0;var e=3&t;return(e=(e=(e=(e=e*(2-(15&t)*e)&15)*(2-(255&t)*e)&255)*(2-((65535&t)*e&65535))&65535)*(2-t*e%this.DV)%this.DV)>0?this.DV-e:-e},i.prototype.isEven=function(){return 0==(this.t>0?1&this[0]:this.s)},i.prototype.exp=function(t,e){if(t>4294967295||t<1)return i.ONE;var r=I(),n=I(),s=e.convert(this),o=C(t)-1;for(s.copyTo(r);--o>=0;)if(e.sqrTo(r,n),(t&1<0)e.mulTo(n,s,r);else{var h=r;r=n,n=h}return e.revert(r)},i.prototype.chunkSize=function(t){return Math.floor(Math.LN2*this.DB/Math.log(t))},i.prototype.toRadix=function(t){if(null==t&&(t=10),0==this.signum()||t<2||t>36)return"0";var e=this.chunkSize(t),i=Math.pow(t,e),r=H(i),n=I(),s=I(),o="";for(this.divRemTo(r,n,s);n.signum()>0;)o=(i+s.intValue()).toString(t).substr(1)+o,n.divRemTo(r,n,s);return s.intValue().toString(t)+o},i.prototype.fromRadix=function(t,e){this.fromInt(0),null==e&&(e=10);for(var r=this.chunkSize(e),n=Math.pow(e,r),s=!1,o=0,h=0,a=0;a=r&&(this.dMultiply(n),this.dAddOffset(h,0),o=0,h=0))}o>0&&(this.dMultiply(Math.pow(e,o)),this.dAddOffset(h,0)),s&&i.ZERO.subTo(this,this)},i.prototype.fromNumber=function(t,e,r){if("number"==typeof e)if(t<2)this.fromInt(1);else for(this.fromNumber(t,r),this.testBit(t-1)||this.bitwiseTo(i.ONE.shiftLeft(t-1),n,this),this.isEven()&&this.dAddOffset(1,0);!this.isProbablePrime(e);)this.dAddOffset(2,0),this.bitLength()>t&&this.subTo(i.ONE.shiftLeft(t-1),this);else{var s=[],o=7&t;s.length=1+(t>>3),e.nextBytes(s),o>0?s[0]&=(1<>=this.DB;if(t.t>=this.DB;r+=this.s}else{for(r+=this.s;i>=this.DB;r+=t.s}e.s=r<0?-1:0,r>0?e[i++]=r:r<-1&&(e[i++]=this.DV+r),e.t=i,e.clamp()},i.prototype.dMultiply=function(t){this[this.t]=this.am(0,t-1,this,0,0,this.t),++this.t,this.clamp()},i.prototype.dAddOffset=function(t,e){if(0!=t){for(;this.t<=e;)this[this.t++]=0;for(this[e]+=t;this[e]>=this.DV;)this[e]-=this.DV,++e>=this.t&&(this[this.t++]=0),++this[e]}},i.prototype.multiplyLowerTo=function(t,e,i){var r=Math.min(this.t+t.t,e);for(i.s=0,i.t=r;r>0;)i[--r]=0;for(var n=i.t-this.t;r=0;)i[r]=0;for(r=Math.max(e-this.t,0);r0)if(0==e)i=this[0]%t;else for(var r=this.t-1;r>=0;--r)i=(e*i+this[r])%t;return i},i.prototype.millerRabin=function(t){var e=this.subtract(i.ONE),r=e.getLowestSetBit();if(r<=0)return!1;var n=e.shiftRight(r);(t=t+1>>1)>D.length&&(t=D.length);for(var s=I(),o=0;o0&&(i.rShiftTo(o,i),r.rShiftTo(o,r));var h=function(){(s=i.getLowestSetBit())>0&&i.rShiftTo(s,i),(s=r.getLowestSetBit())>0&&r.rShiftTo(s,r),i.compareTo(r)>=0?(i.subTo(r,i),i.rShiftTo(1,i)):(r.subTo(i,r),r.rShiftTo(1,r)),i.signum()>0?setTimeout(h,0):(o>0&&r.lShiftTo(o,r),setTimeout((function(){e(r)}),0))};setTimeout(h,10)}},i.prototype.fromNumberAsync=function(t,e,r,s){if("number"==typeof e)if(t<2)this.fromInt(1);else{this.fromNumber(t,r),this.testBit(t-1)||this.bitwiseTo(i.ONE.shiftLeft(t-1),n,this),this.isEven()&&this.dAddOffset(1,0);var o=this,h=function(){o.dAddOffset(2,0),o.bitLength()>t&&o.subTo(i.ONE.shiftLeft(t-1),o),o.isProbablePrime(e)?setTimeout((function(){s()}),0):setTimeout(h,0)};setTimeout(h,0)}else{var a=[],u=7&t;a.length=1+(t>>3),e.nextBytes(a),u>0?a[0]&=(1<=0?t.mod(this.m):t},t.prototype.revert=function(t){return t},t.prototype.reduce=function(t){t.divRemTo(this.m,null,t)},t.prototype.mulTo=function(t,e,i){t.multiplyTo(e,i),this.reduce(i)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}(),A=function(){function t(t){this.m=t,this.mp=t.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<0&&this.m.subTo(e,e),e},t.prototype.revert=function(t){var e=I();return t.copyTo(e),this.reduce(e),e},t.prototype.reduce=function(t){for(;t.t<=this.mt2;)t[t.t++]=0;for(var e=0;e>15)*this.mpl&this.um)<<15)&t.DM;for(t[i=e+this.m.t]+=this.m.am(0,r,t,e,0,this.m.t);t[i]>=t.DV;)t[i]-=t.DV,t[++i]++}t.clamp(),t.drShiftTo(this.m.t,t),t.compareTo(this.m)>=0&&t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,i){t.multiplyTo(e,i),this.reduce(i)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}(),V=function(){function t(t){this.m=t,this.r2=I(),this.q3=I(),R.ONE.dlShiftTo(2*t.t,this.r2),this.mu=this.r2.divide(t)}return t.prototype.convert=function(t){if(t.s<0||t.t>2*this.m.t)return t.mod(this.m);if(t.compareTo(this.m)<0)return t;var e=I();return t.copyTo(e),this.reduce(e),e},t.prototype.revert=function(t){return t},t.prototype.reduce=function(t){for(t.drShiftTo(this.m.t-1,this.r2),t.t>this.m.t+1&&(t.t=this.m.t+1,t.clamp()),this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3),this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2);t.compareTo(this.r2)<0;)t.dAddOffset(1,this.m.t+1);for(t.subTo(this.r2,t);t.compareTo(this.m)>=0;)t.subTo(this.m,t)},t.prototype.mulTo=function(t,e,i){t.multiplyTo(e,i),this.reduce(i)},t.prototype.sqrTo=function(t,e){t.squareTo(e),this.reduce(e)},t}();function I(){return new R(null)}function N(t,e){return new R(t,e)}var P="undefined"!=typeof navigator;P&&"Microsoft Internet Explorer"==navigator.appName?(R.prototype.am=function(t,e,i,r,n,s){for(var o=32767&e,h=e>>15;--s>=0;){var a=32767&this[t],u=this[t++]>>15,c=h*a+u*o;n=((a=o*a+((32767&c)<<15)+i[r]+(1073741823&n))>>>30)+(c>>>15)+h*u+(n>>>30),i[r++]=1073741823&a}return n},T=30):P&&"Netscape"!=navigator.appName?(R.prototype.am=function(t,e,i,r,n,s){for(;--s>=0;){var o=e*this[t++]+i[r]+n;n=Math.floor(o/67108864),i[r++]=67108863&o}return n},T=26):(R.prototype.am=function(t,e,i,r,n,s){for(var o=16383&e,h=e>>14;--s>=0;){var a=16383&this[t],u=this[t++]>>14,c=h*a+u*o;n=((a=o*a+((16383&c)<<14)+i[r]+n)>>28)+(c>>14)+h*u,i[r++]=268435455&a}return n},T=28),R.prototype.DB=T,R.prototype.DM=(1<>>16)&&(t=e,i+=16),0!=(e=t>>8)&&(t=e,i+=8),0!=(e=t>>4)&&(t=e,i+=4),0!=(e=t>>2)&&(t=e,i+=2),0!=(e=t>>1)&&(t=e,i+=1),i}R.ZERO=H(0),R.ONE=H(1);var F,U,K=function(){function t(){this.i=0,this.j=0,this.S=[]}return t.prototype.init=function(t){var e,i,r;for(e=0;e<256;++e)this.S[e]=e;for(i=0,e=0;e<256;++e)i=i+this.S[e]+t[e%t.length]&255,r=this.S[e],this.S[e]=this.S[i],this.S[i]=r;this.i=0,this.j=0},t.prototype.next=function(){var t;return this.i=this.i+1&255,this.j=this.j+this.S[this.i]&255,t=this.S[this.i],this.S[this.i]=this.S[this.j],this.S[this.j]=t,this.S[t+this.S[this.i]&255]},t}(),k=null;if(null==k){k=[],U=0;var _=void 0;if("undefined"!=typeof window&&window.crypto&&window.crypto.getRandomValues){var z=new Uint32Array(256);for(window.crypto.getRandomValues(z),_=0;_=256||U>=256)window.removeEventListener?window.removeEventListener("mousemove",G,!1):window.detachEvent&&window.detachEvent("onmousemove",G);else try{var e=t.x+t.y;k[U++]=255&e,Z+=1}catch(t){}};"undefined"!=typeof window&&(window.addEventListener?window.addEventListener("mousemove",G,!1):window.attachEvent&&window.attachEvent("onmousemove",G))}function $(){if(null==F){for(F=new K;U<256;){var t=Math.floor(65536*Math.random());k[U++]=255&t}for(F.init(k),U=0;U0&&e.length>0?(this.n=N(t,16),this.e=parseInt(e,16)):console.error("Invalid RSA public key")},t.prototype.encrypt=function(t){var e=this.n.bitLength()+7>>3,i=function(t,e){if(e=0&&e>0;){var n=t.charCodeAt(r--);n<128?i[--e]=n:n>127&&n<2048?(i[--e]=63&n|128,i[--e]=n>>6|192):(i[--e]=63&n|128,i[--e]=n>>6&63|128,i[--e]=n>>12|224)}i[--e]=0;for(var s=new Y,o=[];e>2;){for(o[0]=0;0==o[0];)s.nextBytes(o);i[--e]=o[0]}return i[--e]=2,i[--e]=0,new R(i)}(t,e);if(null==i)return null;var r=this.doPublic(i);if(null==r)return null;for(var n=r.toString(16),s=n.length,o=0;o<2*e-s;o++)n="0"+n;return n},t.prototype.setPrivate=function(t,e,i){null!=t&&null!=e&&t.length>0&&e.length>0?(this.n=N(t,16),this.e=parseInt(e,16),this.d=N(i,16)):console.error("Invalid RSA private key")},t.prototype.setPrivateEx=function(t,e,i,r,n,s,o,h){null!=t&&null!=e&&t.length>0&&e.length>0?(this.n=N(t,16),this.e=parseInt(e,16),this.d=N(i,16),this.p=N(r,16),this.q=N(n,16),this.dmp1=N(s,16),this.dmq1=N(o,16),this.coeff=N(h,16)):console.error("Invalid RSA private key")},t.prototype.generate=function(t,e){var i=new Y,r=t>>1;this.e=parseInt(e,16);for(var n=new R(e,16);;){for(;this.p=new R(t-r,1,i),0!=this.p.subtract(R.ONE).gcd(n).compareTo(R.ONE)||!this.p.isProbablePrime(10););for(;this.q=new R(r,1,i),0!=this.q.subtract(R.ONE).gcd(n).compareTo(R.ONE)||!this.q.isProbablePrime(10););if(this.p.compareTo(this.q)<=0){var s=this.p;this.p=this.q,this.q=s}var o=this.p.subtract(R.ONE),h=this.q.subtract(R.ONE),a=o.multiply(h);if(0==a.gcd(n).compareTo(R.ONE)){this.n=this.p.multiply(this.q),this.d=n.modInverse(a),this.dmp1=this.d.mod(o),this.dmq1=this.d.mod(h),this.coeff=this.q.modInverse(this.p);break}}},t.prototype.decrypt=function(t){var e=N(t,16),i=this.doPrivate(e);return null==i?null:function(t,e){for(var i=t.toByteArray(),r=0;r=i.length)return null;for(var n="";++r191&&s<224?(n+=String.fromCharCode((31&s)<<6|63&i[r+1]),++r):(n+=String.fromCharCode((15&s)<<12|(63&i[r+1])<<6|63&i[r+2]),r+=2)}return n}(i,this.n.bitLength()+7>>3)},t.prototype.generateAsync=function(t,e,i){var r=new Y,n=t>>1;this.e=parseInt(e,16);var s=new R(e,16),o=this,h=function(){var e=function(){if(o.p.compareTo(o.q)<=0){var t=o.p;o.p=o.q,o.q=t}var e=o.p.subtract(R.ONE),r=o.q.subtract(R.ONE),n=e.multiply(r);0==n.gcd(s).compareTo(R.ONE)?(o.n=o.p.multiply(o.q),o.d=s.modInverse(n),o.dmp1=o.d.mod(e),o.dmq1=o.d.mod(r),o.coeff=o.q.modInverse(o.p),setTimeout((function(){i()}),0)):setTimeout(h,0)},a=function(){o.q=I(),o.q.fromNumberAsync(n,1,r,(function(){o.q.subtract(R.ONE).gcda(s,(function(t){0==t.compareTo(R.ONE)&&o.q.isProbablePrime(10)?setTimeout(e,0):setTimeout(a,0)}))}))},u=function(){o.p=I(),o.p.fromNumberAsync(t-n,1,r,(function(){o.p.subtract(R.ONE).gcda(s,(function(t){0==t.compareTo(R.ONE)&&o.p.isProbablePrime(10)?setTimeout(a,0):setTimeout(u,0)}))}))};setTimeout(u,0)};setTimeout(h,0)},t.prototype.sign=function(t,e,i){var r=function(t,e){if(e15)throw"ASN.1 length too long to represent by 8x: n = "+t.toString(16);return(128+i).toString(16)+e},this.getEncodedHex=function(){return(null==this.hTLV||this.isModified)&&(this.hV=this.getFreshValueHex(),this.hL=this.getLengthHexFromValue(),this.hTLV=this.hT+this.hL+this.hV,this.isModified=!1),this.hTLV},this.getValueHex=function(){return this.getEncodedHex(),this.hV},this.getFreshValueHex=function(){return""}},W.asn1.DERAbstractString=function(t){W.asn1.DERAbstractString.superclass.constructor.call(this),this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=stohex(this.s)},this.setStringHex=function(t){this.hTLV=null,this.isModified=!0,this.s=null,this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&("string"==typeof t?this.setString(t):void 0!==t.str?this.setString(t.str):void 0!==t.hex&&this.setStringHex(t.hex))},Q.lang.extend(W.asn1.DERAbstractString,W.asn1.ASN1Object),W.asn1.DERAbstractTime=function(t){W.asn1.DERAbstractTime.superclass.constructor.call(this),this.localDateToUTC=function(t){return utc=t.getTime()+6e4*t.getTimezoneOffset(),new Date(utc)},this.formatDate=function(t,e,i){var r=this.zeroPadding,n=this.localDateToUTC(t),s=String(n.getFullYear());"utc"==e&&(s=s.substr(2,2));var o=s+r(String(n.getMonth()+1),2)+r(String(n.getDate()),2)+r(String(n.getHours()),2)+r(String(n.getMinutes()),2)+r(String(n.getSeconds()),2);if(!0===i){var h=n.getMilliseconds();if(0!=h){var a=r(String(h),3);o=o+"."+(a=a.replace(/[0]+$/,""))}}return o+"Z"},this.zeroPadding=function(t,e){return t.length>=e?t:new Array(e-t.length+1).join("0")+t},this.getString=function(){return this.s},this.setString=function(t){this.hTLV=null,this.isModified=!0,this.s=t,this.hV=stohex(t)},this.setByDateValue=function(t,e,i,r,n,s){var o=new Date(Date.UTC(t,e-1,i,r,n,s,0));this.setByDate(o)},this.getFreshValueHex=function(){return this.hV}},Q.lang.extend(W.asn1.DERAbstractTime,W.asn1.ASN1Object),W.asn1.DERAbstractStructured=function(t){W.asn1.DERAbstractString.superclass.constructor.call(this),this.setByASN1ObjectArray=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array=t},this.appendASN1Object=function(t){this.hTLV=null,this.isModified=!0,this.asn1Array.push(t)},this.asn1Array=new Array,void 0!==t&&void 0!==t.array&&(this.asn1Array=t.array)},Q.lang.extend(W.asn1.DERAbstractStructured,W.asn1.ASN1Object),W.asn1.DERBoolean=function(){W.asn1.DERBoolean.superclass.constructor.call(this),this.hT="01",this.hTLV="0101ff"},Q.lang.extend(W.asn1.DERBoolean,W.asn1.ASN1Object),W.asn1.DERInteger=function(t){W.asn1.DERInteger.superclass.constructor.call(this),this.hT="02",this.setByBigInteger=function(t){this.hTLV=null,this.isModified=!0,this.hV=W.asn1.ASN1Util.bigIntToMinTwosComplementsHex(t)},this.setByInteger=function(t){var e=new R(String(t),10);this.setByBigInteger(e)},this.setValueHex=function(t){this.hV=t},this.getFreshValueHex=function(){return this.hV},void 0!==t&&(void 0!==t.bigint?this.setByBigInteger(t.bigint):void 0!==t.int?this.setByInteger(t.int):"number"==typeof t?this.setByInteger(t):void 0!==t.hex&&this.setValueHex(t.hex))},Q.lang.extend(W.asn1.DERInteger,W.asn1.ASN1Object),W.asn1.DERBitString=function(t){if(void 0!==t&&void 0!==t.obj){var e=W.asn1.ASN1Util.newObject(t.obj);t.hex="00"+e.getEncodedHex()}W.asn1.DERBitString.superclass.constructor.call(this),this.hT="03",this.setHexValueIncludingUnusedBits=function(t){this.hTLV=null,this.isModified=!0,this.hV=t},this.setUnusedBitsAndHexValue=function(t,e){if(t<0||7=2?(n[n.length]=s,s=0,o=0):s<<=4}}if(o)throw new Error("Hex encoding incomplete: 4 bits missing");return n}(t):g.unarmor(t),n=E.decode(r);if(3===n.sub.length&&(n=n.sub[2].sub[0]),9===n.sub.length){e=n.sub[1].getHexStringValue(),this.n=N(e,16),i=n.sub[2].getHexStringValue(),this.e=parseInt(i,16);var s=n.sub[3].getHexStringValue();this.d=N(s,16);var o=n.sub[4].getHexStringValue();this.p=N(o,16);var h=n.sub[5].getHexStringValue();this.q=N(h,16);var a=n.sub[6].getHexStringValue();this.dmp1=N(a,16);var c=n.sub[7].getHexStringValue();this.dmq1=N(c,16);var f=n.sub[8].getHexStringValue();this.coeff=N(f,16)}else{if(2!==n.sub.length)return!1;if(n.sub[0].sub){var l=n.sub[1].sub[0];e=l.sub[0].getHexStringValue(),this.n=N(e,16),i=l.sub[1].getHexStringValue(),this.e=parseInt(i,16)}else e=n.sub[0].getHexStringValue(),this.n=N(e,16),i=n.sub[1].getHexStringValue(),this.e=parseInt(i,16)}return!0}catch(t){return!1}},e.prototype.getPrivateBaseKey=function(){var t={array:[new W.asn1.DERInteger({int:0}),new W.asn1.DERInteger({bigint:this.n}),new W.asn1.DERInteger({int:this.e}),new W.asn1.DERInteger({bigint:this.d}),new W.asn1.DERInteger({bigint:this.p}),new W.asn1.DERInteger({bigint:this.q}),new W.asn1.DERInteger({bigint:this.dmp1}),new W.asn1.DERInteger({bigint:this.dmq1}),new W.asn1.DERInteger({bigint:this.coeff})]};return new W.asn1.DERSequence(t).getEncodedHex()},e.prototype.getPrivateBaseKeyB64=function(){return f(this.getPrivateBaseKey())},e.prototype.getPublicBaseKey=function(){var t=new W.asn1.DERSequence({array:[new W.asn1.DERObjectIdentifier({oid:"1.2.840.113549.1.1.1"}),new W.asn1.DERNull]}),e=new W.asn1.DERSequence({array:[new W.asn1.DERInteger({bigint:this.n}),new W.asn1.DERInteger({int:this.e})]}),i=new W.asn1.DERBitString({hex:"00"+e.getEncodedHex()});return new W.asn1.DERSequence({array:[t,i]}).getEncodedHex()},e.prototype.getPublicBaseKeyB64=function(){return f(this.getPublicBaseKey())},e.wordwrap=function(t,e){if(!t)return t;var i="(.{1,"+(e=e||64)+"})( +|$\n?)|(.{1,"+e+"})";return t.match(RegExp(i,"g")).join("\n")},e.prototype.getPrivateKey=function(){var t="-----BEGIN RSA PRIVATE KEY-----\n";return(t+=e.wordwrap(this.getPrivateBaseKeyB64())+"\n")+"-----END RSA PRIVATE KEY-----"},e.prototype.getPublicKey=function(){var t="-----BEGIN PUBLIC KEY-----\n";return(t+=e.wordwrap(this.getPublicBaseKeyB64())+"\n")+"-----END PUBLIC KEY-----"},e.hasPublicKeyProperty=function(t){return(t=t||{}).hasOwnProperty("n")&&t.hasOwnProperty("e")},e.hasPrivateKeyProperty=function(t){return(t=t||{}).hasOwnProperty("n")&&t.hasOwnProperty("e")&&t.hasOwnProperty("d")&&t.hasOwnProperty("p")&&t.hasOwnProperty("q")&&t.hasOwnProperty("dmp1")&&t.hasOwnProperty("dmq1")&&t.hasOwnProperty("coeff")},e.prototype.parsePropertiesFrom=function(t){this.n=t.n,this.e=t.e,t.hasOwnProperty("d")&&(this.d=t.d,this.p=t.p,this.q=t.q,this.dmp1=t.dmp1,this.dmq1=t.dmq1,this.coeff=t.coeff)},e}(J),nt=i(155),st=void 0!==nt?null===(et=nt.env)||void 0===et?void 0:"3.3.1":void 0;const ot=function(){function t(t){void 0===t&&(t={}),t=t||{},this.default_key_size=t.default_key_size?parseInt(t.default_key_size,10):1024,this.default_public_exponent=t.default_public_exponent||"010001",this.log=t.log||!1,this.key=null}return t.prototype.setKey=function(t){this.log&&this.key&&console.warn("A key was already set, overriding existing."),this.key=new rt(t)},t.prototype.setPrivateKey=function(t){this.setKey(t)},t.prototype.setPublicKey=function(t){this.setKey(t)},t.prototype.decrypt=function(t){try{return this.getKey().decrypt(l(t))}catch(t){return!1}},t.prototype.encrypt=function(t){try{return f(this.getKey().encrypt(t))}catch(t){return!1}},t.prototype.sign=function(t,e,i){try{return f(this.getKey().sign(t,e,i))}catch(t){return!1}},t.prototype.verify=function(t,e,i){try{return this.getKey().verify(t,l(e),i)}catch(t){return!1}},t.prototype.getKey=function(t){if(!this.key){if(this.key=new rt,t&&"[object Function]"==={}.toString.call(t))return void this.key.generateAsync(this.default_key_size,this.default_public_exponent,t);this.key.generate(this.default_key_size,this.default_public_exponent)}return this.key},t.prototype.getPrivateKey=function(){return this.getKey().getPrivateKey()},t.prototype.getPrivateKeyB64=function(){return this.getKey().getPrivateBaseKeyB64()},t.prototype.getPublicKey=function(){return this.getKey().getPublicKey()},t.prototype.getPublicKeyB64=function(){return this.getKey().getPublicBaseKeyB64()},t.version=st,t}()})(),r.default})())); \ No newline at end of file diff --git a/assets/map/100.png b/assets/map/100.png new file mode 100644 index 0000000..2639bba Binary files /dev/null and b/assets/map/100.png differ diff --git a/assets/map/50.png b/assets/map/50.png new file mode 100644 index 0000000..e67c5d0 Binary files /dev/null and b/assets/map/50.png differ diff --git a/assets/map/c.png b/assets/map/c.png new file mode 100644 index 0000000..23b2c8d Binary files /dev/null and b/assets/map/c.png differ diff --git a/assets/map/h.png b/assets/map/h.png new file mode 100644 index 0000000..31cec4a Binary files /dev/null and b/assets/map/h.png differ diff --git a/assets/map/hh.png b/assets/map/hh.png new file mode 100644 index 0000000..dc8c775 Binary files /dev/null and b/assets/map/hh.png differ diff --git a/assets/map/index.html b/assets/map/index.html new file mode 100644 index 0000000..5581383 --- /dev/null +++ b/assets/map/index.html @@ -0,0 +1,389 @@ + + + + + 特殊作业扎点 + + + + + + + + + + +
+ + + + + diff --git a/assets/map/jc.png b/assets/map/jc.png new file mode 100644 index 0000000..38bbc8f Binary files /dev/null and b/assets/map/jc.png differ diff --git a/assets/map/l.png b/assets/map/l.png new file mode 100644 index 0000000..165f498 Binary files /dev/null and b/assets/map/l.png differ diff --git a/assets/map/ls.png b/assets/map/ls.png new file mode 100644 index 0000000..fb06e6b Binary files /dev/null and b/assets/map/ls.png differ diff --git a/assets/map/map.html b/assets/map/map.html new file mode 100644 index 0000000..94addd3 --- /dev/null +++ b/assets/map/map.html @@ -0,0 +1,137 @@ + + + + + + 地图 + + + + + +
+ + + diff --git a/assets/tabbar/application.png b/assets/tabbar/application.png new file mode 100644 index 0000000..374d201 Binary files /dev/null and b/assets/tabbar/application.png differ diff --git a/assets/tabbar/application_cur.png b/assets/tabbar/application_cur.png new file mode 100644 index 0000000..0ac6464 Binary files /dev/null and b/assets/tabbar/application_cur.png differ diff --git a/assets/tabbar/basics.png b/assets/tabbar/basics.png new file mode 100644 index 0000000..8522b0f Binary files /dev/null and b/assets/tabbar/basics.png differ diff --git a/assets/tabbar/basics_cur.png b/assets/tabbar/basics_cur.png new file mode 100644 index 0000000..4a985aa Binary files /dev/null and b/assets/tabbar/basics_cur.png differ diff --git a/assets/tabbar/my.png b/assets/tabbar/my.png new file mode 100644 index 0000000..bccbace Binary files /dev/null and b/assets/tabbar/my.png differ diff --git a/assets/tabbar/my_cur.png b/assets/tabbar/my_cur.png new file mode 100644 index 0000000..a001fe2 Binary files /dev/null and b/assets/tabbar/my_cur.png differ diff --git a/assets/tabbar/works.png b/assets/tabbar/works.png new file mode 100644 index 0000000..09a1f09 Binary files /dev/null and b/assets/tabbar/works.png differ diff --git a/assets/tabbar/works_cur.png b/assets/tabbar/works_cur.png new file mode 100644 index 0000000..feaf442 Binary files /dev/null and b/assets/tabbar/works_cur.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..1dc6cf7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..5b28235 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,237 @@ +PODS: + - BaiduMapKit/Base (6.6.4) + - BaiduMapKit/Map (6.6.4): + - BaiduMapKit/Base + - BaiduMapKit/Utils (6.6.4): + - BaiduMapKit/Base + - camera_avfoundation (0.0.1): + - Flutter + - connectivity_plus (0.0.1): + - Flutter + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - flutter_baidu_mapapi_base (3.9.0): + - BaiduMapKit/Utils (= 6.6.4) + - Flutter + - flutter_baidu_mapapi_map (3.9.0): + - BaiduMapKit/Map (= 6.6.4) + - Flutter + - flutter_baidu_mapapi_base + - flutter_baidu_mapapi_utils (3.9.0): + - BaiduMapKit/Utils (= 6.6.4) + - Flutter + - flutter_baidu_mapapi_base + - flutter_native_splash (2.4.3): + - Flutter + - flutter_new_badger (0.0.1): + - Flutter + - fluttertoast (0.0.2): + - Flutter + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - image_picker_ios (0.0.1): + - Flutter + - 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): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - pdfx (1.0.0): + - Flutter + - permission_handler_apple (9.3.0): + - Flutter + - photo_manager (3.8.0): + - Flutter + - FlutterMacOS + - SDWebImage (5.21.1): + - SDWebImage/Core (= 5.21.1) + - SDWebImage/Core (5.21.1) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.5) + - url_launcher_ios (0.0.1): + - Flutter + - video_compress (0.3.0): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - flutter_baidu_mapapi_base (from `.symlinks/plugins/flutter_baidu_mapapi_base/ios`) + - flutter_baidu_mapapi_map (from `.symlinks/plugins/flutter_baidu_mapapi_map/ios`) + - flutter_baidu_mapapi_utils (from `.symlinks/plugins/flutter_baidu_mapapi_utils/ios`) + - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_new_badger (from `.symlinks/plugins/flutter_new_badger/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - 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`) + - pdfx (from `.symlinks/plugins/pdfx/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - photo_manager (from `.symlinks/plugins/photo_manager/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_compress (from `.symlinks/plugins/video_compress/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +SPEC REPOS: + trunk: + - BaiduMapKit + - DKImagePickerController + - DKPhotoGallery + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + camera_avfoundation: + :path: ".symlinks/plugins/camera_avfoundation/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + flutter_baidu_mapapi_base: + :path: ".symlinks/plugins/flutter_baidu_mapapi_base/ios" + flutter_baidu_mapapi_map: + :path: ".symlinks/plugins/flutter_baidu_mapapi_map/ios" + flutter_baidu_mapapi_utils: + :path: ".symlinks/plugins/flutter_baidu_mapapi_utils/ios" + flutter_native_splash: + :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_new_badger: + :path: ".symlinks/plugins/flutter_new_badger/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/darwin" + image_picker_ios: + :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: + :path: ".symlinks/plugins/open_file_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + pdfx: + :path: ".symlinks/plugins/pdfx/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + photo_manager: + :path: ".symlinks/plugins/photo_manager/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_compress: + :path: ".symlinks/plugins/video_compress/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" + +SPEC CHECKSUMS: + BaiduMapKit: 84991811cb07b24c6ead7d59022c13245427782c + camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_baidu_mapapi_base: 24dd82034374c6f52a73e90316834c63ff8d4f64 + flutter_baidu_mapapi_map: f799cc1bb3d39196b8d3d59399ca8635e690bd44 + flutter_baidu_mapapi_utils: 0c69394243d51e97f521f396e150aaaf31e84e29 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_new_badger: 133aaf93e9a5542bf905c8483d8b83c5ef4946ea + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 + nfc_manager: f6d5609c09b4640b914a3dc67479a2e392965fd0 + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 + open_file_ios: 5ff7526df64e4394b4fe207636b67a95e83078bb + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + pdfx: 77f4dddc48361fbb01486fa2bdee4532cbb97ef3 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + photo_manager: 343d78032bf7ebe944d2ab9702204dc2eda07338 + SDWebImage: f29024626962457f3470184232766516dee8dfea + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_compress: f2133a07762889d67f0711ac831faa26f956980e + video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b3c07f8 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,777 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D5BBE0FD0EAD05B450AEE37D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D535373899AA9DD1E6E7511 /* Pods_RunnerTests.framework */; }; + F6873A5EEA3A9CD4CC30B8F1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1EE4D2BDFD716FB80E3EB92 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1D535373899AA9DD1E6E7511 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 45D597292E2F804B009DEAE7 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 71118A79D1B878EF92EF0C82 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A56B7CF3BE602CF776E716CF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + B32FA449F9E8B4334D347232 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + D892E03EBCFFE7AEA26BADEF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E32A50F59BB884B8EEEA48BB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EB96BCD274E74DA46B53876B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F1EE4D2BDFD716FB80E3EB92 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F6873A5EEA3A9CD4CC30B8F1 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9AD71197BA5C33D24636B03A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D5BBE0FD0EAD05B450AEE37D /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + D1B24C997F4A0101CD2D397B /* Pods */, + DB502A4E3FC0608DF67A6BF0 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 45D597292E2F804B009DEAE7 /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + D1B24C997F4A0101CD2D397B /* Pods */ = { + isa = PBXGroup; + children = ( + D892E03EBCFFE7AEA26BADEF /* Pods-Runner.debug.xcconfig */, + 71118A79D1B878EF92EF0C82 /* Pods-Runner.release.xcconfig */, + B32FA449F9E8B4334D347232 /* Pods-Runner.profile.xcconfig */, + E32A50F59BB884B8EEEA48BB /* Pods-RunnerTests.debug.xcconfig */, + EB96BCD274E74DA46B53876B /* Pods-RunnerTests.release.xcconfig */, + A56B7CF3BE602CF776E716CF /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + DB502A4E3FC0608DF67A6BF0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F1EE4D2BDFD716FB80E3EB92 /* Pods_Runner.framework */, + 1D535373899AA9DD1E6E7511 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + AB8878D34A77D2374C7913D0 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 9AD71197BA5C33D24636B03A /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DCD396AFC5D4EF84A1AB88FB /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 9D4AB6A98949D5BF69896B41 /* [CP] Embed Pods Frameworks */, + 278BCE4BA5CD53A70DAEC00A /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 278BCE4BA5CD53A70DAEC00A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 9D4AB6A98949D5BF69896B41 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + AB8878D34A77D2374C7913D0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + DCD396AFC5D4EF84A1AB88FB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "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; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E32A50F59BB884B8EEEA48BB /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8AKCJ9LW7D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EB96BCD274E74DA46B53876B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8AKCJ9LW7D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A56B7CF3BE602CF776E716CF /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8AKCJ9LW7D; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "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; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "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; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.company.myapp2; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "qa-zsaq"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c2476be --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,94 @@ +import UIKit +import Flutter +import AVFoundation + +@main +@objc class AppDelegate: FlutterAppDelegate { + // 动态方向掩码(默认竖屏) + static var orientationMask: UIInterfaceOrientationMask = .portrait + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + let controller = window?.rootViewController as! FlutterViewController + + // 1) orientation channel (你已有) + let orientationChannel = FlutterMethodChannel(name: "app.orientation", + binaryMessenger: controller.binaryMessenger) + orientationChannel.setMethodCallHandler { [weak self] call, result in + guard let self = self else { return } + + if call.method == "setOrientation" { + guard let arg = call.arguments as? String else { + result(FlutterError(code: "BAD_ARGS", message: "need 'landscape' | 'portrait'", details: nil)) + return + } + + // 先更新允许的方向掩码 + if arg == "landscape" { + AppDelegate.orientationMask = .landscape + } else if arg == "portrait" { + AppDelegate.orientationMask = .portrait + } else { + result(FlutterError(code: "BAD_ARGS", message: "unknown arg", details: nil)) + return + } + + // 再请求实际旋转 + if #available(iOS 16.0, *) { + // 通知顶层 VC:其 supportedInterfaceOrientations 需要刷新 + self.window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + + if let scene = self.window?.windowScene { + let orientations: UIInterfaceOrientationMask = + (arg == "landscape") ? .landscape : .portrait + do { + try scene.requestGeometryUpdate(.iOS(interfaceOrientations: orientations)) + } catch { + result(FlutterError(code: "GEOMETRY_UPDATE_FAILED", + message: error.localizedDescription, details: nil)) + return + } + } + } else { + let target: UIInterfaceOrientation = + (arg == "landscape") ? .landscapeLeft : .portrait + UIDevice.current.setValue(target.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } + + result(true) + } else { + result(FlutterMethodNotImplemented) + } + } + + // 2) permissions channel (新增): 用于 iOS 原生请求相机访问权限 + let permissionChannel = FlutterMethodChannel(name: "qhd_prevention/permissions", + binaryMessenger: controller.binaryMessenger) + permissionChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in + if call.method == "requestCameraAccess" { + // AVCaptureDevice.requestAccess 会在首次调用时触发系统权限弹窗 + AVCaptureDevice.requestAccess(for: .video) { granted in + DispatchQueue.main.async { + result(granted) + } + } + } else { + result(FlutterMethodNotImplemented) + } + } + + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + // 关键:把当前的掩码提供给系统 + override func application(_ application: UIApplication, + supportedInterfaceOrientationsFor window: UIWindow?) + -> UIInterfaceOrientationMask { + return AppDelegate.orientationMask + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d0d98aa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..5fa3a42 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..09a4baa Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..13b9886 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f9dacfd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..74c9145 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..470a8e1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..772149b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..13b9886 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..a30a68d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..27d32eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..feece6d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..4fa3790 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..bf80268 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..9e5fc9d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..27d32eb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..fe44b93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..5337d5a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..342548c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..a2dd400 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..ae77c36 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..c429f45 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 0000000..9f447e1 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 0000000..4ebb677 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..00cabce --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..e815fd6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..e815fd6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..e815fd6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..55c0cae --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..13a000c --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,91 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + 秦港双控 + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + baidumap + + LSRequiresIPhoneOS + + NFCReaderUsageDescription + 需要NFC权限来读取和写入标签 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBluetoothAlwaysUsageDescription + app需要蓝牙权限连接设备 + NSCameraUsageDescription + app需要相机权限来扫描二维码 + NSContactsUsageDescription + app需要通讯录权限添加好友 + NSHealthShareUsageDescription + app需要读取健康数据 + NSHealthUpdateUsageDescription + app需要写入健康数据 + NSLocalNetworkUsageDescription + app需要发现本地网络设备 + NSLocationAlwaysAndWhenInUseUsageDescription + 我们需要访问您的位置来提供基于位置的服务,例如获取位置展示地图。您的位置数据不会用于其他目的。 + NSLocationAlwaysUsageDescription + 我们需要访问您的位置来提供基于位置的服务,例如获取位置展示地图。您的位置数据不会用于其他目的。 + NSLocationWhenInUseUsageDescription + 我们需要访问您的位置来提供基于位置的服务,例如获取位置展示地图。您的位置数据不会用于其他目的。 + NSMicrophoneUsageDescription + app需要麦克风权限进行语音通话 + NSMotionUsageDescription + app需要访问运动数据统计步数 + NSPhotoLibraryAddUsageDescription + app需要保存图片到相册 + NSPhotoLibraryUsageDescription + app需要访问相册以上传图片 + NSUserNotificationsUsageDescription + app需要发送通知提醒重要信息 + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + com.apple.developer.nfc.readersession.formats + + TAG + NDEF + + + \ No newline at end of file diff --git a/ios/Runner/Resources/InfoPlist.strings b/ios/Runner/Resources/InfoPlist.strings new file mode 100644 index 0000000..497d27e --- /dev/null +++ b/ios/Runner/Resources/InfoPlist.strings @@ -0,0 +1 @@ +"CFBundleDisplayName" = "秦港相关方"; \ No newline at end of file diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..2bb4dee --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.nfc.readersession.formats + + TAG + + + diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/Model/list_model.dart b/lib/Model/list_model.dart new file mode 100644 index 0000000..f1abb98 --- /dev/null +++ b/lib/Model/list_model.dart @@ -0,0 +1,6 @@ +/// 清单检查记录 +class RecordCheckModel { + final String title; + final String time; + RecordCheckModel(this.title, this.time); +} \ No newline at end of file diff --git a/lib/common/route_aware_state.dart b/lib/common/route_aware_state.dart new file mode 100644 index 0000000..4bd9bdd --- /dev/null +++ b/lib/common/route_aware_state.dart @@ -0,0 +1,60 @@ +// lib/common/route_aware_state.dart +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:qhd_prevention/common/route_observer.dart'; + +/// 可复用的 RouteAware State 基类 +/// 使用方式:class _MyPageState extends RouteAwareState { ... } +abstract class RouteAwareState extends State with RouteAware { + /// 当页面真正可见(首次 push 或从上层 pop 返回)时会调用 + @protected + FutureOr onVisible(); + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final ModalRoute? route = ModalRoute.of(context); + if (route is PageRoute) { + routeObserver.subscribe(this, route); + } + } + + @override + void dispose() { + try { + routeObserver.unsubscribe(this); + } catch (_) {} + super.dispose(); + } + + @override + void didPush() { + // 页面被 push 到栈顶(首次展示) + _safeCallOnVisible(); + } + + @override + void didPopNext() { + // 从上层 pop 回到当前页面(页面再次可见) + _safeCallOnVisible(); + } + + @override + void didPushNext() { + // 当前页面被新的页面覆盖(不可见) + super.didPushNext(); + } + + void _safeCallOnVisible() { + // 延后到当前帧完成后执行,避免在 build 期间触发 setState 导致断言 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + try { + final r = onVisible(); + if (r is Future) { + r.catchError((_) {}); + } + } catch (_) {} + }); + } +} diff --git a/lib/common/route_model.dart b/lib/common/route_model.dart new file mode 100644 index 0000000..c822df8 --- /dev/null +++ b/lib/common/route_model.dart @@ -0,0 +1,62 @@ +// models/route_model.dart +import 'dart:convert'; + +class RouteModel { + final String target; + final List children; + final bool hasMenu; + final String parentId; + final String routeId; + final String component; + final String path; + final String title; + final String parentIds; + final String meta; + final String routeOrder; + + RouteModel({ + required this.target, + required this.children, + required this.hasMenu, + required this.parentId, + required this.routeId, + required this.component, + required this.title, + required this.path, + required this.parentIds, + required this.meta, + required this.routeOrder, + }); + + factory RouteModel.fromJson(Map json) { + return RouteModel( + target: json['target'] ?? '', + children: (json['children'] as List? ?? []) + .map((child) => RouteModel.fromJson(child)) + .toList(), + hasMenu: json['hasMenu'] ?? false, + parentId: json['parent_ID'] ?? '', + routeId: json['route_ID'] ?? '', + component: json['component'] ?? '', + parentIds: json['parent_IDS'] ?? '', + meta: json['meta'] ?? '', + path: json['path'] ?? '', + title: json['path'] ?? '', + routeOrder: json['route_ORDER'] ?? '0', + ); + } + + // // 解析meta字段获取title + // String get title { + // if (meta.isEmpty) return ''; + // try { + // final metaMap = jsonDecode(meta) as Map; + // return metaMap['title'] ?? ''; + // } catch (e) { + // return ''; + // } + // } + + // 判断是否是叶子节点(没有子节点的路由) + bool get isLeaf => children.isEmpty; +} \ No newline at end of file diff --git a/lib/common/route_observer.dart b/lib/common/route_observer.dart new file mode 100644 index 0000000..0756d32 --- /dev/null +++ b/lib/common/route_observer.dart @@ -0,0 +1,5 @@ +// lib/common/route_observer.dart +import 'package:flutter/widgets.dart'; + +/// 全局 RouteObserver(放在单独文件以避免循环导入) +final RouteObserver routeObserver = RouteObserver(); diff --git a/lib/common/route_service.dart b/lib/common/route_service.dart new file mode 100644 index 0000000..15d022d --- /dev/null +++ b/lib/common/route_service.dart @@ -0,0 +1,58 @@ +import 'package:qhd_prevention/common/route_model.dart'; +/// 路由管理 +class RouteService { + static final RouteService _instance = RouteService._internal(); + factory RouteService() => _instance; + RouteService._internal(); + + // 存储所有路由配置 + List _allRoutes = []; + + // 获取主Tab路由(第一级children) + List get mainTabs => _allRoutes.isNotEmpty + ? _allRoutes.first.children + : []; + + // 初始化路由配置 + void initializeRoutes(List routeList) { + _allRoutes = routeList.map((route) => RouteModel.fromJson(route)).toList(); + } + + // 根据路径查找路由 + RouteModel? findRouteByPath(String path) { + for (final route in _allRoutes) { + final found = _findRouteRecursive(route, path); + if (found != null) return found; + } + return null; + } + + RouteModel? _findRouteRecursive(RouteModel route, String path) { + if (route.path == path) return route; + for (final child in route.children) { + final found = _findRouteRecursive(child, path); + if (found != null) return found; + } + return null; + } + + // 获取某个Tab下的所有可显示的路由(hasMenu为true的叶子节点) + List getRoutesForTab(RouteModel tab) { + final routes = []; + _collectLeafRoutes(tab, routes); + return routes; + } + + void _collectLeafRoutes(RouteModel route, List collector) { + if (route.hasMenu) { + collector.add(route); + if (!route.isLeaf) { + for (final child in route.children) { + _collectLeafRoutes(child, collector); + } + } + } + } + + +} \ No newline at end of file diff --git a/lib/constants/app_enums.dart b/lib/constants/app_enums.dart new file mode 100644 index 0000000..cc7bf9c --- /dev/null +++ b/lib/constants/app_enums.dart @@ -0,0 +1,322 @@ +/// 文件上传类型枚举 +/// +/// 该枚举定义了系统中所有支持的文件上传类型,包含类型标识符和对应的存储路径 +/// 使用示例: +/// ```dart +/// // 通过类型获取枚举 +/// UploadFileType? fileType = UploadFileType.fromType('102'); +/// +/// // 通过路径获取枚举 +/// UploadFileType? fileType = UploadFileType.fromPath('hidden_danger_video'); +/// +/// // 直接使用枚举值 +/// String type = UploadFileType.userAvatar.type; // '13' +/// String path = UploadFileType.userAvatar.path; // 'user_avatar' +/// ``` +enum UploadFileType { + /// 隐患照片 - 类型: '3', 路径: 'hidden_danger_photo' + hiddenDangerPhoto('3', 'hidden_danger_photo'), + + /// 隐患整改照片 - 类型: '4', 路径: 'hidden_danger_rectification_photo' + hiddenDangerRectificationPhoto('4', 'hidden_danger_rectification_photo'), + + /// 隐患验证照片 - 类型: '5', 路径: 'hidden_danger_verification_photo' + hiddenDangerVerificationPhoto('5', 'hidden_danger_verification_photo'), + + /// 证书照片 - 类型: '6', 路径: 'certificate_photo' + certificatePhoto('6', 'certificate_photo'), + + /// 受限空间平面图 - 类型: '7', 路径: 'confined_space_plan' + confinedSpacePlan('7', 'confined_space_plan'), + + /// 隐患整改方案图 - 类型: '8', 路径: 'hidden_danger_rectification_plan' + hiddenDangerRectificationPlan('8', 'hidden_danger_rectification_plan'), + + /// 有限空间确认人签字 - 类型: '9', 路径: 'confined_space_confirmation_signature' + confinedSpaceConfirmationSignature('9', 'confined_space_confirmation_signature'), + + /// 劳动合同图片 - 类型: '10', 路径: 'labor_contract_image' + laborContractImage('10', 'labor_contract_image'), + + /// 商业保险图片 - 类型: '11', 路径: 'commercial_insurance_image' + commercialInsuranceImage('11', 'commercial_insurance_image'), + + /// 证书信息 - 类型: '12', 路径: 'certificate_information' + certificateInformation('12', 'certificate_information'), + + /// 用户头像 - 类型: '13', 路径: 'user_avatar' + userAvatar('13', 'user_avatar'), + + /// 身份证照片 - 类型: '14', 路径: 'id_card_photo' + idCardPhoto('14', 'id_card_photo'), + + /// 社保卡照片 - 类型: '15', 路径: 'social_security_card_photo' + socialSecurityCardPhoto('15', 'social_security_card_photo'), + + /// 工伤保险凭证 - 类型: '16', 路径: 'work_related_injury_insurance_certificate' + workRelatedInjuryInsuranceCertificate('16', 'work_related_injury_insurance_certificate'), + + /// 特种设备巡检照片 - 类型: '17', 路径: 'special_equipment_inspection_photo' + specialEquipmentInspectionPhoto('17', 'special_equipment_inspection_photo'), + + /// 人员证书 - 类型: '18', 路径: 'personnel_certificate' + personnelCertificate('18', 'personnel_certificate'), + + /// 三级教育培训 - 类型: '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'), + + /// 重大危险源报警处置后照片 - 类型: '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'), + + /// 智能门禁外来车辆行驶证照片 - 类型: '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'), + + /// 隐患延期临时措施附件 - 类型: '101', 路径: 'hidden_danger_extension_temporary_measures_attachment' + hiddenDangerExtensionTemporaryMeasuresAttachment('101', 'hidden_danger_extension_temporary_measures_attachment'), + + /// 隐患视频 - 类型: '102', 路径: 'hidden_danger_video' + hiddenDangerVideo('102', 'hidden_danger_video'), + + /// 盲板位置图 - 类型: '105', 路径: 'blind_plate_position_map' + blindPlatePositionMap('105', 'blind_plate_position_map'), + + /// 临时处置信息 - 类型: '106', 路径: 'temporary_disposal_information' + temporaryDisposalInformation('106', 'temporary_disposal_information'), + + /// 整改建议及方案 - 类型: '107', 路径: 'rectification_suggestions_and_plan' + rectificationSuggestionsAndPlan('107', 'rectification_suggestions_and_plan'), + + /// 重大隐患上传隐患调查报告 - 类型: '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'), + + /// 较大隐患整改-临时处置措施 - 类型: '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'), + + /// 补充重大隐患信息 - 类型: '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'), + + /// 报警图片 - 类型: '114', 路径: 'alarm_photo' + alarmPhoto('114', 'alarm_photo'), + + /// 消防器材类型合格表中照片 - 类型: '115', 路径: 'fire_equipment_type_qualification_photo' + fireEquipmentTypeQualificationPhoto('115', 'fire_equipment_type_qualification_photo'), + + /// 动火人图片 - 类型: '116', 路径: 'hot_work_personnel_photo' + hotWorkPersonnelPhoto('116', 'hot_work_personnel_photo'), + + /// 安全承诺书签名 - 类型: '117', 路径: 'safety_pledge_signature' + safetyPledgeSignature('117', 'safety_pledge_signature'), + + /// 动火单位负责人确认签字 - 类型: '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'), + + /// 动火许可证签发单位签字 - 类型: '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'), + + /// 现场负责人确实签字 - 类型: '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'), + + /// 延迟监火图片 - 类型: '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'), + + /// 检查人确认签字 - 类型: '127', 路径: 'inspector_confirmation_signature' + inspectorConfirmationSignature('127', 'inspector_confirmation_signature'), + + /// 被检查人确认签字 - 类型: '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'), + + /// (港股安委办主任)安全总监签发 - 类型: '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'), + + /// 安全总监审批 - 类型: '132', 路径: 'safety_director_approval' + safetyDirectorApproval('132', 'safety_director_approval'), + + /// 分公司安全总监审批 - 类型: '133', 路径: 'branch_safety_director_approval' + branchSafetyDirectorApproval('133', 'branch_safety_director_approval'), + + /// 项目主管部门负责人审核 - 类型: '134', 路径: 'project_authority_review_signature' + projectAuthorityReviewSignature('134', 'project_authority_review_signature'), + + /// 分公司主要负责人签批 - 类型: '135', 路径: 'branch_manager_approval_signature' + branchManagerApprovalSignature('135', 'branch_manager_approval_signature'), + + /// 事故/事件 - 类型: '136', 路径: 'accident_incident' + accidentIncident1('136', 'accident_incident'), + + /// 事故/事件 - 类型: '137', 路径: 'accident_incident' + accidentIncident2('137', 'accident_incident'), + + /// 隐患处置方案 - 类型: '138', 路径: 'hidden_disposal_plan' + hiddenDisposalPlan('138', 'hidden_disposal_plan'), + + /// 安全环保检查-检查签字 - 类型: '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'), + + /// 安全环保检查-检查人签字 - 类型: '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'), + + /// 安全环保检查-被检查文件 - 类型: '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'), + + /// 清单检查合格 - 类型: '145', 路径: 'qualified_list_inspection' + qualifiedListInspection('145', 'qualified_list_inspection'), + + /// 安全环保检查-验收 - 类型: '146', 路径: 'safety_environmental_inspection_acceptance' + safetyEnvironmentalInspectionAcceptance('146', 'safety_environmental_inspection_acceptance'), + + /// 隐患清单排查查签字 - 类型: '147', 路径: 'hidden_qualified_listInspection_signature' + hiddenQualifiedListInspectionSignature('147', 'hidden_qualified_listInspection_signature'), + + /// 安全承诺书签字 - 类型: '150', 路径: 'promise_bookmark_photo' + promiseBookmarkPhoto('150', 'promise_bookmark_photo'), + + /// 项目相关资料 - 类型: '151', 路径: 'project_related_materials' + projectRelatedMaterials('151', 'project_related_materials'), + + /// 人脸识别图片上传 - 类型: '300', 路径: 'facial_recognition_images' + facialRecognitionImages('300', 'facial_recognition_images'), + /// ai识别图片 - 类型: '301', 路径: 'ai_recognition_images' + aiRecognitionImages('301', 'ai_recognition_images'), + + + /// 门口门禁车辆行驶证照片 - 类型: '601', 路径: 'gate_access_vehicle_license_photo' + gateAccessVehicleLicensePhoto('601', 'gate_access_vehicle_license_photo'), + + /// 门口门禁车辆车辆照片 - 类型: '602', 路径: 'gate_access_vehicle_photo' + gateAccessVehiclePhoto('602', 'gate_access_vehicle_photo'), + + /// 门口门禁车辆附件 - 类型: '603', 路径: 'gate_access_vehicle_attachment' + gateAccessVehicleAttachment('603', 'gate_access_vehicle_attachment'), + + /// 排放标准证明 - 类型: '604', 路径: 'emission_standard_certificate' + emissionStandardCertificate('604', 'emission_standard_certificate'), + + /// 机动车登记证书(绿本) - 类型: '605', 路径: 'motor_vehicle_registration_certificate_green_book' + motorVehicleRegistrationCertificateGreenBook('605', 'motor_vehicle_registration_certificate_green_book'); + + const UploadFileType(this.type, this.path); + + /// 文件类型标识符(字符串格式) + final String type; + + /// 文件存储路径 + final String path; + + /// 通过类型字符串查找对应的枚举值 + /// + /// 参数: + /// - [type]: 文件类型字符串,如 '102' + /// + /// 返回值: + /// - 对应的 UploadFileType 枚举值,如果未找到则返回 null + /// + /// 示例: + /// ```dart + /// UploadFileType? fileType = UploadFileType.fromType('102'); + /// if (fileType != null) { + /// print(fileType.path); // 输出: hidden_danger_video + /// } + /// ``` + static UploadFileType? fromType(String type) { + try { + return values.firstWhere((element) => element.type == type); + } catch (e) { + return null; + } + } + + /// 通过路径查找对应的枚举值 + /// + /// 参数: + /// - [path]: 文件存储路径,如 'hidden_danger_video' + /// + /// 返回值: + /// - 对应的 UploadFileType 枚举值,如果未找到则返回 null + /// + /// 示例: + /// ```dart + /// UploadFileType? fileType = UploadFileType.fromPath('hidden_danger_video'); + /// if (fileType != null) { + /// print(fileType.type); // 输出: 102 + /// } + /// ``` + static UploadFileType? fromPath(String path) { + try { + return values.firstWhere((element) => element.path == path); + } catch (e) { + return null; + } + } + + /// 获取所有文件类型字符串的列表 + /// + /// 返回值: + /// - 包含所有文件类型字符串的列表 + /// + /// 示例: + /// ```dart + /// List allTypes = UploadFileType.allTypes; + /// print(allTypes.contains('102')); // 输出: true + /// ``` + static List get allTypes => values.map((e) => e.type).toList(); + + /// 获取所有文件存储路径的列表 + /// + /// 返回值: + /// - 包含所有文件存储路径的列表 + /// + /// 示例: + /// ```dart + /// List allPaths = UploadFileType.allPaths; + /// print(allPaths.contains('hidden_danger_video')); // 输出: true + /// ``` + static List get allPaths => values.map((e) => e.path).toList(); +} \ No newline at end of file diff --git a/lib/customWidget/BaiDuMap/BaiduMapWebView.dart b/lib/customWidget/BaiDuMap/BaiduMapWebView.dart new file mode 100644 index 0000000..1079e85 --- /dev/null +++ b/lib/customWidget/BaiDuMap/BaiduMapWebView.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; + +/// 可复用的地图 WebView 组件 +/// BaiduMapWebView( +/// controller: _controller, +/// isLoading: _isLoading, +/// errorMessage: _errorMessage, +/// onRetry: _initLocation, +/// ) +/// ``` +class BaiduMapWebView extends StatelessWidget { + final WebViewController? controller; + final bool isLoading; + final String? errorMessage; + final VoidCallback onRetry; + + const BaiduMapWebView({ + super.key, + required this.controller, + required this.isLoading, + required this.errorMessage, + required this.onRetry, + }); + + @override + Widget build(BuildContext context) { + if (errorMessage != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 50), + const SizedBox(height: 16), + Text( + errorMessage!, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 24), + CustomButton(text: '重试', backgroundColor: Colors.blue, onPressed: onRetry) + ]), + ), + ); + } + + if (isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + // 非加载且无错误时,controller 应该存在;若为空则显示提示 + if (controller == null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.map_outlined, size: 48), + const SizedBox(height: 8), + const Text('地图未初始化'), + const SizedBox(height: 12), + CustomButton(text: '重试初始化', backgroundColor: Colors.blue, onPressed: onRetry,), + ], + ), + ); + } + + return WebViewWidget(controller: controller!); + } +} diff --git a/lib/customWidget/BaiDuMap/Map_page.dart b/lib/customWidget/BaiDuMap/Map_page.dart new file mode 100644 index 0000000..98f0dd0 --- /dev/null +++ b/lib/customWidget/BaiDuMap/Map_page.dart @@ -0,0 +1,197 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/BaiDuMap/BaiduMapWebView.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/location_service.dart'; +import 'package:qhd_prevention/tools/coord_convert.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:geolocator/geolocator.dart'; + +class MapPage extends StatefulWidget { + final String gson; + + const MapPage({super.key, required this.gson}); + + @override + State createState() => _MapPageState(); +} + +class _MapPageState extends State { + late final WebViewController _controller; + bool _isLoading = true; + String? _errorMessage; + double? _longitude; + double? _latitude; + List _gsonList = []; + late Map mapData = {}; + @override + void initState() { + super.initState(); + _loadWebView(); + } + + /// 获取定位(并初始化 WebView 控制器) + // Future _initLocation() async { + // setState(() { + // _isLoading = true; + // _errorMessage = null; + // }); + // Map prefs = geographicCentroid(_gsonList); + // _loadWebView(LocationResult(latitude: prefs['lat'].toString(), longitude: prefs['lon'].toString())); + // + // } + Future _loadWebView() async { + // 解析 gson + try { + final parsed = jsonDecode(widget.gson); + if (parsed is List) { + _gsonList = parsed; + } else { + _gsonList = []; + } + } catch (e) { + debugPrint('解析 gson 失败: $e'); + _gsonList = []; + } + if (!mounted) return; + Map prefs = geographicCentroid(_gsonList); + // _loadWebView(LocationResult(latitude: prefs['lat'].toString(), longitude: prefs['lon'].toString())); + setState(() { + _longitude = prefs['lon']; + _latitude = prefs['lat']; + }); + + + // 初始化 WebViewController 并加载本地页面 + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + // 注册 JS 通道 'JS' —— 对应 HTML 中的 window.JS.postMessage(...) + ..addJavaScriptChannel('JS', onMessageReceived: (dynamic message) { + // message 是动态对象,不在签名中引用具体类型以避免 "Undefined class" 问题 + String payload; + try { + payload = message.message ?? message.toString(); + } catch (e) { + payload = message.toString(); + } + _onJsMessage(payload); + }) + // 也保留一个备用通道 'Flutter' + ..addJavaScriptChannel('Flutter', onMessageReceived: (dynamic message) { + String payload; + try { + payload = message.message ?? message.toString(); + } catch (e) { + payload = message.toString(); + } + _onJsMessage(payload); + }) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (String url) async { + debugPrint('网页加载完成: $url'); + await _injectLocationParams(); + }, + onWebResourceError: (err) { + debugPrint('Web resource error: ${err.description}'); + }, + ), + ); + + // 加载本地 assets 中的 HTML + // await _controller.loadFlutterAsset('assets/map/index.html'); + await _controller.loadRequest(Uri.parse('http://47.92.102.56:7811/file/fluteightmap/index.html')); + + setState(() { + _isLoading = false; + }); + } + + /// 注入参数并调用页面初始化函数 window.initWithData(...) + Future _injectLocationParams() async { + if (_longitude == null || _latitude == null) { + debugPrint('位置尚未准备好,跳过注入'); + return; + } + + final params = { + 'longitude': _longitude, + 'latitude': _latitude, + 'GSON': _gsonList, + 't': DateTime.now().millisecondsSinceEpoch, + }; + debugPrint('网页初始化参数: ${jsonEncode(params)}'); + + final jsonParams = jsonEncode(params); + + try { + await _controller.runJavaScript(''' + (function(){ + try { + if (typeof window.initWithData === 'function') { + window.initWithData($jsonParams); + } else if (typeof window.initMap === 'function') { + window.initMap($jsonParams); + } else { + console.error('initWithData / initMap function not found'); + } + } catch(e) { + console.error('call initWithData error', e); + } + })(); + '''); + debugPrint('已注入地图初始化参数'); + } catch (e) { + debugPrint('注入位置参数失败: $e'); + } + } + + /// 处理来自 Web 的消息(字符串或 JSON) + void _onJsMessage(String message) { + debugPrint('收到来自 Web 的消息: $message'); + if (message.isEmpty) return; + try { + Map data = jsonDecode(message); + if (FormUtils.hasValue(data, 'ok') && data['ok'] == true) { + }else{ + if (FormUtils.hasValue(data, 'type') && data['type'] == 'converted') { + setState(() { + mapData = data; + }); + }else{ + ToastUtil.showNormal(context, '当前选择点位不在区域中'); + setState(() { + mapData = {}; + }); + } + } + } catch (_) { + setState(() { + mapData = {}; + }); + ToastUtil.showNormal(context, '当前选择点位不在区域中'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: '地图选择', actions: [ + if (mapData.isNotEmpty) + TextButton(onPressed: (){ + Navigator.of(context).pop((mapData)); + }, child: Text('确定', style: TextStyle(color: Colors.white, fontSize: 17),)) + ],), + body: SafeArea( + child: BaiduMapWebView( + controller: _isLoading || _errorMessage != null ? null : _controller, + isLoading: _isLoading, + errorMessage: _errorMessage, + onRetry: _loadWebView, + ),), + ); + } +} diff --git a/lib/customWidget/BaiDuMap/map_webview_page.dart b/lib/customWidget/BaiDuMap/map_webview_page.dart new file mode 100644 index 0000000..316bc67 --- /dev/null +++ b/lib/customWidget/BaiDuMap/map_webview_page.dart @@ -0,0 +1,488 @@ +// lib/pages/map_webview_page.dart +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class MapWebViewPage extends StatefulWidget { + const MapWebViewPage({this.canEdit = true,this.oldLongitude = '',this.oldLatitude = '', Key? key}) : super(key: key); + + final bool canEdit; + final String oldLongitude; + final String oldLatitude; + + @override + State createState() => _MapWebViewPageState(); +} + +class _MapWebViewPageState extends State { + late final WebViewController _controller; + bool _loading = true; + String _mapUrl = ''; + double? _selectedLongitude; + double? _selectedLatitude; + bool _locationError = false; + + // 默认坐标(北京) + static const double defaultLongitude = 116.397428; + static const double defaultLatitude = 39.90923; + + @override + void initState() { + super.initState(); + _initializeWebView(); + _initializeMap(); + } + + void _initializeWebView() { + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..addJavaScriptChannel('FlutterChannel', onMessageReceived: _onMessageReceived) + ..setNavigationDelegate(NavigationDelegate( + onPageStarted: (url) { + debugPrint('页面开始加载: $url'); + }, + onPageFinished: (url) { + debugPrint('页面加载完成: $url'); + setState(() { + _loading = false; + }); + _injectFlutterBridge(); + }, + onWebResourceError: (error) { + debugPrint('Web资源错误: ${error.errorCode} - ${error.description}'); + setState(() { + _loading = false; + }); + }, + onNavigationRequest: (request) { + debugPrint('导航请求: ${request.url}'); + return NavigationDecision.navigate; + }, + )); + } + + Future _initializeMap() async { + try { + debugPrint('开始获取位置信息...'); + + // 先设置默认URL,避免长时间等待 + final defaultCoord = _gcj02ToBd09(defaultLongitude, defaultLatitude); + _setMapUrl(defaultCoord[0], defaultCoord[1]); + + var position ; + if(widget.canEdit){ + // 异步获取当前位置 + position = await _getCurrentLocationWithTimeout(); + }else{ + try{ + double oldLongitude = double.parse(widget.oldLongitude); + double oldLatitude = double.parse(widget.oldLatitude); + position = Position(longitude: oldLongitude, latitude:oldLatitude , + timestamp: DateTime.now(), accuracy: 0.0, altitude: 0.0, altitudeAccuracy: 0.0, heading: 0.0, + headingAccuracy: 0.0, speed: 0.0, speedAccuracy: 0.0); + }catch (e) { + // 异步获取当前位置 + position = await _getCurrentLocationWithTimeout(); + } + } + + if (position != null) { + debugPrint('成功获取位置: ${position.longitude}, ${position.latitude}'); + // GCJ02 -> BD09 坐标转换 + final bd09Coord = _gcj02ToBd09(position.longitude, position.latitude); + _setMapUrl(bd09Coord[0], bd09Coord[1]); + } else { + debugPrint('使用默认位置'); + _showToast('使用默认位置,您可以手动选择位置'); + } + + // 加载地图 + await _controller.loadRequest(Uri.parse(_mapUrl)); + + } catch (e) { + debugPrint('地图初始化失败: $e'); + // 即使出错也继续加载地图,使用默认位置 + _showToast('位置获取失败,使用默认位置'); + await _controller.loadRequest(Uri.parse(_mapUrl)); + } + } + + Future _getCurrentLocationWithTimeout() async { + try { + // 首先检查定位服务是否开启 + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + debugPrint('定位服务未开启'); + _showToast('定位服务未开启,使用默认位置'); + return null; + } + + // 检查权限 + LocationPermission permission = await Geolocator.checkPermission(); + debugPrint('当前定位权限: $permission'); + + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + debugPrint('请求后定位权限: $permission'); + } + + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + debugPrint('定位权限被拒绝'); + _showToast('定位权限被拒绝,使用默认位置'); + return null; + } + + // 设置超时 + final position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.best, + ).timeout(const Duration(seconds: 10)); + + return position; + } catch (e) { + debugPrint('获取位置异常: $e'); + // 尝试获取最后已知位置 + try { + final lastPosition = await Geolocator.getLastKnownPosition(); + if (lastPosition != null) { + debugPrint('使用最后已知位置: ${lastPosition.longitude}, ${lastPosition.latitude}'); + return lastPosition; + } + } catch (e) { + debugPrint('获取最后已知位置失败: $e'); + } + return null; + } + } + + void _setMapUrl(double longitude, double latitude) { + final timestamp = DateTime.now().millisecondsSinceEpoch; + setState(() { + _mapUrl = 'https://skqhdg.porthebei.com:9004/map/map.html?' + 'longitude=$longitude&' + 'latitude=$latitude&' + 't=$timestamp'; + }); + debugPrint('地图URL: $_mapUrl'); + } + + void _injectFlutterBridge() { + const bridgeScript = ''' + // 重写uni.postMessage以发送到Flutter + if (typeof uni !== 'undefined') { + const originalPostMessage = uni.postMessage; + uni.postMessage = function(data) { + console.log('uni.postMessage被调用:', data); + // 发送到Flutter + if (window.FlutterChannel) { + try { + window.FlutterChannel.postMessage(JSON.stringify(data)); + } catch(e) { + console.log('发送到Flutter失败:', e); + } + } + // 保持原有逻辑 + if (typeof originalPostMessage === 'function') { + originalPostMessage(data); + } + }; + + // 确保plus环境检测返回true + uni.getEnv = function(callback) { + if (typeof callback === 'function') { + callback({ plus: true }); + } + }; + } + + // 添加Flutter专用的消息发送方法 + window.sendToFlutter = function(data) { + console.log('sendToFlutter被调用:', data); + if (window.FlutterChannel) { + try { + window.FlutterChannel.postMessage(JSON.stringify(data)); + } catch(e) { + console.log('发送到Flutter失败:', e); + } + } + }; + + console.log('Flutter bridge注入完成'); + '''; + + _controller.runJavaScript(bridgeScript).catchError((error) { + debugPrint('注入Flutter bridge失败: $error'); + }); + } + + void _onMessageReceived(JavaScriptMessage message) { + try { + debugPrint('收到原始消息: ${message.message}'); + final data = jsonDecode(message.message); + debugPrint('解析后的消息: $data'); + + // 解析坐标数据 + if (data != null) { + final coords = data['data']; + setState(() { + _selectedLongitude = coords['longitue']; + _selectedLatitude = coords['latitude']; + }); + + debugPrint('选中坐标: $_selectedLongitude, $_selectedLatitude'); + } else { + debugPrint('无法从消息中提取坐标'); + } + } catch (e) { + debugPrint('解析地图消息失败: $e'); + debugPrint('原始消息内容: ${message.message}'); + } + } + + Map? _extractCoordinates(dynamic data) { + try { + debugPrint('开始提取坐标,数据类型: ${data.runtimeType}'); + + if (data is Map) { + // 处理不同的数据结构 + dynamic coordsData = data; + + // 处理嵌套结构 + if (data.containsKey('data') && data['data'] is List && data['data'].isNotEmpty) { + coordsData = data['data'][0]; + debugPrint('从data数组中提取坐标数据'); + } + + if (coordsData is Map) { + debugPrint('坐标数据键: ${coordsData.keys}'); + + // 处理拼写错误 (longitue -> longitude) + final longitude = _toDouble(coordsData['longitude']) ?? + _toDouble(coordsData['longitue']); + final latitude = _toDouble(coordsData['latitude']); + + debugPrint('解析结果 - 经度: $longitude, 纬度: $latitude'); + + if (longitude != null && latitude != null) { + return { + 'longitude': longitude, + 'latitude': latitude, + }; + } + } + } else if (data is String) { + // 尝试从字符串中提取坐标 + debugPrint('尝试从字符串中提取坐标'); + final coordPattern = RegExp(r'[-+]?\d+\.\d+'); + final matches = coordPattern.allMatches(data).toList(); + if (matches.length >= 2) { + final longitude = double.tryParse(matches[0].group(0)!); + final latitude = double.tryParse(matches[1].group(0)!); + if (longitude != null && latitude != null) { + return { + 'longitude': longitude, + 'latitude': latitude, + }; + } + } + } + } catch (e) { + debugPrint('提取坐标失败: $e'); + } + return null; + } + + double? _toDouble(dynamic value) { + if (value == null) return null; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) { + // 处理可能的字符串格式 + final cleaned = value.replaceAll(RegExp(r'[^\d.-]'), ''); + return double.tryParse(cleaned); + } + return null; + } + + void _showToast(String message) { + if (!mounted) return; + ToastUtil.showNormal(context, message); + } + + Future _confirmSelection() async { + if (_selectedLongitude == null || _selectedLatitude == null) { + // 如果没有选中位置,尝试从页面获取当前位置 + await _getCurrentLocationFromPage(); + } + + if (_selectedLongitude != null && _selectedLatitude != null) { + final result = { + 'longitude': _selectedLongitude!, + 'latitude': _selectedLatitude!, + }; + + debugPrint('返回坐标结果: $result'); + Navigator.of(context).pop(result); + } else { + _showToast('请先在地图上选择位置'); + } + } + + Future _getCurrentLocationFromPage() async { + try { + debugPrint('尝试从页面获取选中位置'); + const getterScript = ''' + (function(){ + try { + // 尝试多种方式获取选中位置 + if (typeof getSelectedLocation === 'function') { + var result = getSelectedLocation(); + if (result) return JSON.stringify(result); + } + + if (window.selectedLocation) { + return JSON.stringify(window.selectedLocation); + } + + // 如果没有选中位置,返回地图中心 + if (map && typeof map.getCenter === 'function') { + var center = map.getCenter(); + return JSON.stringify({ + longitude: center.getLng(), + latitude: center.getLat() + }); + } + + return null; + } catch(e) { + console.log('获取位置错误:', e); + return null; + } + })(); + '''; + + final result = await _controller.runJavaScriptReturningResult(getterScript); + debugPrint('页面返回的位置结果: $result'); + + if (result != null) { + String resultString = result.toString(); + // 处理可能的双重编码 + if (resultString.startsWith('"') && resultString.endsWith('"')) { + resultString = resultString.substring(1, resultString.length - 1); + } + + final coords = _extractCoordinates(jsonDecode(resultString)); + if (coords != null) { + setState(() { + _selectedLongitude = coords['longitude']; + _selectedLatitude = coords['latitude']; + }); + debugPrint('从页面获取到坐标: $_selectedLongitude, $_selectedLatitude'); + } + } + } catch (e) { + debugPrint('从页面获取位置失败: $e'); + } + } + + void _retryLocation() async { + setState(() { + _loading = true; + _locationError = false; + }); + + await _initializeMap(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar( + title: '地图选点', + actions: [ + if (_selectedLongitude != null&&widget.canEdit) + TextButton( + onPressed: _confirmSelection, + child: const Text( + '确定', + style: TextStyle(color: Colors.white, fontSize: 17), + ), + ), + ], + ), + body: Column( + children: [ + // 状态提示栏 + if (_locationError) + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + color: Colors.orange[100], + child: Row( + children: [ + Icon(Icons.warning_amber, color: Colors.orange[800], size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + '定位失败,使用默认位置', + style: TextStyle(color: Colors.orange[800], fontSize: 12), + ), + ), + TextButton( + onPressed: _retryLocation, + child: Text( + '重试', + style: TextStyle(color: Colors.orange[800], fontSize: 12), + ), + ), + ], + ), + ), + + // 地图区域 + Expanded( + child: Stack( + children: [ + if (_mapUrl.isNotEmpty) + WebViewWidget(controller: _controller), + + if (_loading) + const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('地图加载中...'), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + // GCJ02 -> BD09 坐标转换 + List _gcj02ToBd09(double lng, double lat) { + const double xPi = math.pi * 3000.0 / 180.0; + final double z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * xPi); + final double theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * xPi); + final double bdLng = z * math.cos(theta) + 0.0065; + final double bdLat = z * math.sin(theta) + 0.006; + return [bdLng, bdLat]; + } + + @override + void dispose() { + super.dispose(); + debugPrint('地图页面销毁'); + } +} \ No newline at end of file diff --git a/lib/customWidget/DangerPartsPicker.dart b/lib/customWidget/DangerPartsPicker.dart new file mode 100644 index 0000000..59279ce --- /dev/null +++ b/lib/customWidget/DangerPartsPicker.dart @@ -0,0 +1,510 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; + +// 数据字典模型 +class DictCategory { + final String id; + final String name; + final Map extValues; + final String parentId; + final String hiddenregionId; + final String hiddenregion; + final String? responsibleDeptName; + final String? responsibleDeptId; + final String? responsibleUserName; + final String? responsibleUserId; + final int sortindex; + final String? comments; + final String? corpinfoId; + final String? sbdl; + final String? sbmc; + final List? children; + + DictCategory({ + required this.id, + required this.name, + required this.extValues, + required this.parentId, + required this.hiddenregionId, + required this.hiddenregion, + this.responsibleDeptName, + this.responsibleDeptId, + this.responsibleUserName, + this.responsibleUserId, + required this.sortindex, + this.comments, + this.corpinfoId, + this.sbdl, + this.sbmc, + this.children, + }); + + factory DictCategory.fromJson(Map json) { + // 安全读取并兼容字符串或数字类型的 id + String parseString(dynamic v) { + if (v == null) return ''; + if (v is String) return v; + return v.toString(); + } + + int parseInt(dynamic v) { + if (v == null) return 0; + if (v is int) return v; + if (v is String) return int.tryParse(v) ?? 0; + return 0; + } + + // 处理子节点 + final rawChildren = json['children']; + List? childrenList; + if (rawChildren is List && rawChildren.isNotEmpty) { + try { + childrenList = rawChildren + .where((e) => e != null) + .map((e) => DictCategory.fromJson(Map.from(e as Map))) + .toList(); + } catch (e) { + childrenList = null; + } + } + + // 处理扩展值 + final extRaw = json['extValues']; + Map extMap = {}; + if (extRaw is Map) { + extMap = Map.from(extRaw); + } + + return DictCategory( + id: parseString(json['id']), + name: parseString(json['hiddenregion']), // 使用 hiddenregion 作为显示名称 + extValues: extMap, + parentId: parseString(json['parentId']), + hiddenregionId: parseString(json['hiddenregionId']), + hiddenregion: parseString(json['hiddenregion']), + responsibleDeptName: parseString(json['responsibleDeptName']), + responsibleDeptId: parseString(json['responsibleDeptId']), + responsibleUserName: parseString(json['responsibleUserName']), + responsibleUserId: parseString(json['responsibleUserId']), + sortindex: parseInt(json['sortindex']), + comments: parseString(json['comments']), + corpinfoId: parseString(json['corpinfoId']), + sbdl: parseString(json['sbdl']), + sbmc: parseString(json['sbmc']), + children: childrenList, + ); + } + + // 转换为Map,便于使用 + Map toMap() { + return { + 'id': id, + 'name': name, + 'hiddenregionId': hiddenregionId, + 'hiddenregion': hiddenregion, + 'parentId': parentId, + 'responsibleDeptName': responsibleDeptName, + 'responsibleDeptId': responsibleDeptId, + 'responsibleUserName': responsibleUserName, + 'responsibleUserId': responsibleUserId, + 'sortindex': sortindex, + 'comments': comments, + 'corpinfoId': corpinfoId, + 'sbdl': sbdl, + 'sbmc': sbmc, + 'extValues': extValues, + }; + } +} + +/// 数据字典选择器回调签名 +typedef DictSelectCallback = void Function(String id, String name, Map? extraData); + +class DangerPartsPicker extends StatefulWidget { + + /// 回调,返回选中项的 id, name 和额外数据 + final DictSelectCallback onSelected; + + /// 是否显示搜索框 + final bool showSearch; + + /// 标题 + final String title; + + /// 确认按钮文本 + final String confirmText; + + /// 取消按钮文本 + final String cancelText; + + const DangerPartsPicker({ + Key? key, + required this.onSelected, + this.showSearch = true, + this.title = '请选择', + this.confirmText = '确定', + this.cancelText = '取消', + }) : super(key: key); + + @override + _DangerPartsPickerState createState() => _DangerPartsPickerState(); +} + +class _DangerPartsPickerState extends State { + String selectedId = ''; + String selectedName = ''; + Map? selectedExtraData; + Set expandedSet = {}; + + List original = []; + List filtered = []; + bool loading = true; + bool error = false; + String errorMessage = ''; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + selectedId = ''; + selectedName = ''; + expandedSet = {}; + _searchController.addListener(_onSearchChanged); + _loadDictData(); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + Future _loadDictData() async { + try { + setState(() { + loading = true; + error = false; + }); + + final result = await HiddenDangerApi.getHiddenDangerAreas(); + final raw = result['data'] as List; + setState(() { + original = raw.map((e) => DictCategory.fromJson(e as Map)).toList(); + filtered = original; + loading = false; + }); + } catch (e) { + setState(() { + loading = false; + error = true; + errorMessage = e.toString(); + }); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase().trim(); + setState(() { + filtered = query.isEmpty ? original : _filterCategories(original, query); + // 搜索时展开所有节点以便查看结果 + if (query.isNotEmpty) { + expandedSet.addAll(_getAllExpandableIds(filtered)); + } + }); + } + + Set _getAllExpandableIds(List categories) { + Set ids = {}; + for (var category in categories) { + if (category.children != null && category.children!.isNotEmpty) { + ids.add(category.id); + ids.addAll(_getAllExpandableIds(category.children!)); + } + } + return ids; + } + + List _filterCategories(List list, String query) { + List result = []; + for (var cat in list) { + List? children; + if (cat.children != null) { + children = _filterCategories(cat.children!, query); + } + + if (cat.name.toLowerCase().contains(query) || + (children != null && children.isNotEmpty)) { + result.add( + DictCategory( + id: cat.id, + name: cat.name, + children: children, + extValues: cat.extValues, + parentId: cat.parentId, + hiddenregionId: cat.hiddenregionId, + hiddenregion: cat.hiddenregion, + responsibleDeptName: cat.responsibleDeptName, + responsibleDeptId: cat.responsibleDeptId, + responsibleUserName: cat.responsibleUserName, + responsibleUserId: cat.responsibleUserId, + sortindex: cat.sortindex, + comments: cat.comments, + corpinfoId: cat.corpinfoId, + sbdl: cat.sbdl, + sbmc: cat.sbmc, + ), + ); + } + } + return result; + } + + Widget _buildRow(DictCategory category, int indent) { + final hasChildren = category.children != null && category.children!.isNotEmpty; + final isExpanded = expandedSet.contains(category.id); + final isSelected = category.id == selectedId; + + return Column( + children: [ + InkWell( + onTap: () { + setState(() { + if (hasChildren) { + isExpanded + ? expandedSet.remove(category.id) + : expandedSet.add(category.id); + } + selectedId = category.id; + selectedName = category.name; + selectedExtraData = category.toMap(); + }); + }, + child: Container( + color: Colors.white, + child: Row( + children: [ + SizedBox(width: 16.0 * indent), + SizedBox( + width: 24, + child: hasChildren + ? Icon( + isExpanded + ? Icons.arrow_drop_down_rounded + : Icons.arrow_right_rounded, + size: 35, + color: Colors.grey[600], + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: Colors.black, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: Colors.blue, + ), + ), + ], + ), + ), + ), + if (hasChildren && isExpanded) + ...category.children!.map((child) => _buildRow(child, indent + 1)), + ], + ); + } + + Widget _buildTitleBar() { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Center( + child: Text( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + Widget _buildActionBar() { + return Container( + color: Colors.white, + 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), + ), + ), + ), + + // 搜索框(如果有搜索功能) + if (widget.showSearch) ...[ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SearchBarWidget( + controller: _searchController, + isShowSearchButton: false, + onSearch: (keyboard) {}, + ), + ), + ), + ] else ...[ + const Expanded(child: SizedBox()), + ], + + // 确定按钮 + GestureDetector( + onTap: selectedId.isEmpty + ? null + : () { + Navigator.of(context).pop(); + widget.onSelected(selectedId, selectedName, selectedExtraData); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + widget.confirmText, + style: TextStyle( + fontSize: 16, + color: selectedId.isEmpty ? Colors.grey : Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildContent() { + if (loading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('加载中...'), + ], + ), + ); + } + + if (error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Colors.red), + 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, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadDictData, + child: const Text('重试'), + ), + ], + ), + ); + } + + if (filtered.isEmpty) { + 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)), + ], + ), + ); + } + + return Container( + color: Colors.white, + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + 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), + ), + ], + ), + child: Column( + children: [ + // 标题行 + // _buildTitleBar(), + // 操作行(取消、搜索、确定) + _buildActionBar(), + const Divider(height: 1), + + // 内容区域 + Expanded( + child: _buildContent(), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/DocumentPicker.dart b/lib/customWidget/DocumentPicker.dart new file mode 100644 index 0000000..532d10c --- /dev/null +++ b/lib/customWidget/DocumentPicker.dart @@ -0,0 +1,270 @@ +// 封装文件:document_picker.dart +// 说明:只支持从相册选图片(单张或多张)和选择文件(pdf/任意文件),不支持拍照。 +// 同时在内部封装了一个从底部弹出的选择框(“从相册获取”、“选择文件”、“取消”), +// 并在调用相册选择前检查权限(若未授权会引导用户去设置)。 + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:file_picker/file_picker.dart'; + +/// 选中的文件统一结构 +class SelectedFile { + final String name; + final String? path; // 若无法直接拿到物理文件,则为 null + final Uint8List? bytes; // 当 path 不可用时,可能通过 bytes 提供内容 + final int? size; // 字节数 + final String? mimeType; // 可选 mime + final SourceType source; + + SelectedFile({ + required this.name, + this.path, + this.bytes, + this.size, + this.mimeType, + required this.source, + }); + + bool get hasFile => path != null || bytes != null; +} + +enum SourceType { gallery, assetPicker, filePicker } + +class DocumentPicker { + DocumentPicker._(); // 不可实例化,使用静态方法 + + static final ImagePicker _imagePicker = ImagePicker(); + + /// 检查并请求相册/媒体库权限。 + /// 返回 true 表示有权限可访问,false 表示拒绝或受限(limited 也视为可用)。 + static Future ensurePhotoPermission() async { + final PermissionState ps = await PhotoManager.requestPermissionExtend(); + return ps.isAuth || ps == PermissionState.limited; + } + + /// 单张从相册选择(image_picker) + static Future pickSingleImageFromGallery({int? maxSizeInBytes}) async { + try { + final XFile? xfile = await _imagePicker.pickImage(source: ImageSource.gallery, imageQuality: 90); + if (xfile == null) return null; + final File f = File(xfile.path); + final int size = await f.length(); + if (maxSizeInBytes != null && size > maxSizeInBytes) return null; + final bytes = await f.readAsBytes(); + return SelectedFile( + name: xfile.name, + path: xfile.path, + bytes: bytes, + size: size, + mimeType: null, + source: SourceType.gallery, + ); + } catch (e) { + debugPrint('pickSingleImageFromGallery error: $e'); + return null; + } + } + + /// 多选图片(推荐使用 wechat_assets_picker) + static Future> pickAssets({ + required BuildContext context, + int maxAssets = 9, + int? maxSizeInBytes, + }) async { + try { + final hasAuth = await ensurePhotoPermission(); + if (!hasAuth) { + // 未授权,返回空,需要调用者处理引导用户 + debugPrint('Photo permission denied'); + return []; + } + + final List? result = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + maxAssets: maxAssets, + requestType: RequestType.image, + specialPickerType: SpecialPickerType.noPreview, + ), + ); + + if (result == null || result.isEmpty) return []; + + final List out = []; + for (final asset in result) { + final File? file = await asset.file; + if (file != null) { + final int size = await file.length(); + if (maxSizeInBytes != null && size > maxSizeInBytes) continue; + final bytes = await file.readAsBytes(); + out.add(SelectedFile( + name: file.path.split('/').last, + path: file.path, + bytes: bytes, + size: size, + mimeType: null, + source: SourceType.assetPicker, + )); + } else { + final Uint8List? originData = await asset.originBytes; + if (originData == null) continue; + final int size = originData.lengthInBytes; + if (maxSizeInBytes != null && size > maxSizeInBytes) continue; + out.add(SelectedFile( + name: '${asset.id}.jpg', + path: null, + bytes: originData, + size: size, + mimeType: null, + source: SourceType.assetPicker, + )); + } + } + return out; + } catch (e) { + debugPrint('pickAssets error: $e'); + return []; + } + } + + /// 选择任意文件(如 pdf/doc 等) + static Future> pickFiles({ + bool allowMultiple = false, + List? allowedExtensions, + int? maxSizeInBytes, + }) async { + try { + final FileType ft = (allowedExtensions == null) ? FileType.any : FileType.custom; + + final FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: allowMultiple, + type: ft, + allowedExtensions: allowedExtensions, + withData: true, + ); + + if (result == null) return []; + + final List out = []; + for (final pf in result.files) { + final int size = pf.size ?? (pf.bytes?.lengthInBytes ?? 0); + if (maxSizeInBytes != null && size > maxSizeInBytes) continue; + out.add(SelectedFile( + name: pf.name, + path: pf.path, + bytes: pf.bytes, + size: size, + mimeType: pf.extension, + source: SourceType.filePicker, + )); + } + + return out; + } catch (e) { + debugPrint('pickFiles error: $e'); + return []; + } + } + + /// 小工具:把 SelectedFile 转换为上传需要的 File (如果 path 可用),否则保存 bytes 到临时文件并返回 File + static Future toFile(SelectedFile sf) async { + try { + if (sf.path != null) return File(sf.path!); + if (sf.bytes != null) { + final temp = await File('${Directory.systemTemp.path}/${sf.name}').create(); + await temp.writeAsBytes(sf.bytes!); + return temp; + } + return null; + } catch (e) { + debugPrint('toFile error: $e'); + return null; + } + } + + /// 底部弹窗,用户从 "从相册获取" / "选择文件" / "取消" 中选择。 + /// 该方法会在用户选择后直接执行对应的选择逻辑并返回选中的文件列表。 + /// - maxAssets: 多选图片时的最大数量 + /// - maxSizeInBytes: 单个文件最大字节数限制 + /// - allowedExtensions: 选择文件时允许的扩展名(为空表示不限制) + static Future> showPickerModal( + BuildContext context, { + int maxAssets = 9, + int? maxSizeInBytes, + List? allowedExtensions, + bool allowMultipleFiles = false, + }) async { + final choice = await showModalBottomSheet( + context: context, + builder: (c) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Center(child: Text('从相册获取')), + onTap: () => Navigator.of(c).pop('gallery'), + ), + const Divider(height: 1), + ListTile( + title: const Center(child: Text('选择文件')), + onTap: () => Navigator.of(c).pop('file'), + ), + const Divider(height: 1), + ListTile( + title: const Center(child: Text('取消')), + onTap: () => Navigator.of(c).pop(null), + ), + ], + ), + ); + }, + ); + + if (choice == null) return []; + + if (choice == 'gallery') { + final hasAuth = await ensurePhotoPermission(); + if (!hasAuth) { + // 未授权,提示并引导用户去设置 + await showDialog( + context: context, + builder: (d) { + return AlertDialog( + title: const Text('权限未开启'), + content: const Text('应用暂无相册权限,是否去设置开启?'), + actions: [ + TextButton(onPressed: () => Navigator.of(d).pop(), child: const Text('取消')), + TextButton( + onPressed: () { + PhotoManager.openSetting(); + Navigator.of(d).pop(); + }, + child: const Text('去设置'), + ), + ], + ); + }, + ); + return []; + } + + // 先提供多选(wechat_assets_picker),如果你只想要单选可替换为 pickSingleImageFromGallery + final images = await pickAssets(context: context, maxAssets: maxAssets, maxSizeInBytes: maxSizeInBytes); + return images; + } + + if (choice == 'file') { + final files = await pickFiles(allowMultiple: allowMultipleFiles, allowedExtensions: allowedExtensions, maxSizeInBytes: maxSizeInBytes); + return files; + } + + return []; + } +} diff --git a/lib/customWidget/IconBadgeButton.dart b/lib/customWidget/IconBadgeButton.dart new file mode 100644 index 0000000..6ea47e4 --- /dev/null +++ b/lib/customWidget/IconBadgeButton.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; + +class IconBadgeButton extends StatelessWidget { + final String iconPath; + final String title; + final int unreadCount; + final VoidCallback onTap; + final double width; // 可选:按钮宽度(默认 80) + + const IconBadgeButton({ + Key? key, + required this.iconPath, + required this.title, + required this.onTap, + this.unreadCount = 0, + this.width = 80, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // badge 文本,超过 99 展示 99+ + final String badgeText = unreadCount.toString(); + + // 文字两行的高度估算(可按需微调) + const double titleLineHeight = 18.0; + const double titleMaxLines = 2; + final double titleHeight = titleLineHeight * titleMaxLines + 2; // +2 兜底 padding + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: SizedBox( + width: width, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 图标 + 角标(固定高度容器) + SizedBox( + width: 50, + height: 50, + child: Stack( + clipBehavior: Clip.none, + children: [ + // 圆形背景图标(始终居中) + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: const Color(0xFFE8F4FD), + borderRadius: BorderRadius.circular(25), + ), + child: Center( + child: Image.asset( + iconPath, + width: 30, + height: 30, + fit: BoxFit.contain, + ), + ), + ), + + // 角标(只有 unreadCount > 0 时显示) + if (unreadCount > 0) + Positioned( + top: -6, + right: -6, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5), + constraints: const BoxConstraints(minWidth: 18, minHeight: 18), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(9), + border: Border.all(color: Colors.white, width: 1.5), + ), + alignment: Alignment.center, + child: Text( + badgeText, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + + // 固定高度的文字区域(最多两行),保持图标位置不变 + SizedBox( + height: titleHeight, + child: Center( + child: Text( + title, + style: const TextStyle( + fontSize: 12, + color: Colors.black87, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/customWidget/ItemWidgetFactory.dart b/lib/customWidget/ItemWidgetFactory.dart new file mode 100644 index 0000000..03d1733 --- /dev/null +++ b/lib/customWidget/ItemWidgetFactory.dart @@ -0,0 +1,704 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; + +import '../http/ApiService.dart'; +import '../tools/tools.dart'; + +/// 自定义组件 +class ListItemFactory { + /// 类型1:横向spaceBetween布局两个文本加按钮 + static Widget createRowSpaceBetweenItem({ + required String leftText, + required String rightText, + double verticalPadding = 10, + double horizontalPadding = 0, + Color textColor = Colors.black, + bool isRight = false, + bool isRequired = false, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(isRequired?'* ':' ', style: TextStyle(color: Colors.red)), + Text( + leftText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: textColor, + ), + ), + ], + ), + if (isRight) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + fit: FlexFit.loose, + child: Text( + _truncateText(rightText,20) , + style: TextStyle(fontSize: 13, color: Colors.black54), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + SizedBox(width: 6), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.black45, + size: 15, + ), + ], + ) + else + Flexible( + fit: FlexFit.loose, + child: Text( + _truncateText(rightText,17) , + style: TextStyle(fontSize: 13, color: Colors.grey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + } + + // 辅助函数:截断文本 + static String _truncateText(String text, int maxLength) { + if (text.length <= maxLength) return text; + return text.substring(0, maxLength) + '...'; + } + + ///类型2:上下布局两个文本(自适应高度) + static Widget createColumnTextItem({ + required String topText, + required String bottomText, + double verticalPadding = 15, + double horizontalPadding = 0, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + topText, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 5), + Text( + bottomText, + style: TextStyle(fontSize: 13, color: Colors.grey), + softWrap: true, + maxLines: null, // 允许无限行数 + ), + ], + ), + ); + } + + /// 类型3:文本和图片上下布局 + static Widget createTextImageItem({ + required String text, + required List imageUrls, + double imageHeight = 90, + double verticalPadding = 10, + double horizontalPadding = 0, + // 点击图片时回调,index 为被点击图片的下标 + void Function(int index)? onImageTapped, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + Wrap( + spacing: 8, // 水平间距 + runSpacing: 8, // 垂直间距 + children: List.generate(imageUrls.length, (i) { + final url = ApiService.baseImgPath + imageUrls[i]; + Widget img; + if (url.startsWith('http')) { + img = Image.network( + url, + height: imageHeight, + width: imageHeight * 3 / 2, + fit: BoxFit.fill, + ); + } else { + img = Image.asset( + url, + height: imageHeight, + width: imageHeight * 3 / 2, + fit: BoxFit.fill, + ); + } + return GestureDetector( + onTap: () { + if (onImageTapped != null) onImageTapped(i); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: img, + ), + ); + }), + ), + ], + ), + ); + } + + /// 类型6:文本和视频上下布局 + static Widget createTextVideoItem({ + required String text, + required String videoUrl, + double videoHeight = 90, + double verticalPadding = 10, + double horizontalPadding = 0, + VoidCallback? onVideoTapped, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 10), + videoUrl.isNotEmpty + ? GestureDetector( + onTap: onVideoTapped, + child: Container( + height: videoHeight, + width: videoHeight * 3 / 2, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4), + ), + child: const Center( + child: Icon( + Icons.play_circle_outline, + size: 40, + color: Colors.white, + ), + ), + ), + ) + : SizedBox(height: 10), + ], + ), + ); + } + + ///类型4:一个文本(自适应高度) + static Widget createAloneTextItem({ + required String text, + double verticalPadding = 10, + double horizontalPadding = 0, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + text, + style: TextStyle(fontSize: 13, color: Colors.grey), + softWrap: true, + maxLines: null, // 允许无限行数 + ), + ], + ), + ); + } + + /// YesNo + static Widget createYesNoSection({ + required String title, + String yesLabel = '是', + String noLabel = '否', + required bool? groupValue, + required ValueChanged onChanged, + double verticalPadding = 15, + double horizontalPadding = 10, + bool isEdit = true, + String text = '', + bool isRequired = false, + }) { + return Padding( + padding: EdgeInsets.only( + top: 0, + right: horizontalPadding, + left: horizontalPadding, + bottom: verticalPadding, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: isEdit ? 0 : verticalPadding), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + if (isRequired && isEdit) + Text('* ', style: TextStyle(color: Colors.red)), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + ), + if (isEdit) + Row( + children: [ + Row( + children: [ + Radio( + activeColor: Colors.blue, + value: true, + groupValue: groupValue, + onChanged: (val) => onChanged(val!), + ), + Text(yesLabel), + ], + ), + const SizedBox(width: 5), + Row( + children: [ + Radio( + activeColor: Colors.blue, + value: false, + groupValue: groupValue, + onChanged: (val) => onChanged(val!), + ), + Text(noLabel), + ], + ), + ], + ), + if (!isEdit) Text(text, style: TextStyle()), + ], + ), + ), + ); + } + /// 多个radio按钮 + static Widget createMultiOptionSection({ + required String title, + required List options, + required String? selectedValue, + required ValueChanged onChanged, + double verticalPadding = 15, + double horizontalPadding = 10, + bool isEdit = true, + String text = '', + bool isRequired = false, + Axis direction = Axis.horizontal, // 排列方向:水平或垂直 + double spacing = 8.0, // 选项间距 + }) { + return Padding( + padding: EdgeInsets.only( + top: verticalPadding, + right: horizontalPadding, + left: horizontalPadding, + bottom: verticalPadding, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: isEdit ? 0 : verticalPadding), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isRequired && isEdit) + Text('* ', style: TextStyle(color: Colors.red)), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + ], + ), + ), + if (isEdit) + _buildOptions( + options: options, + selectedValue: selectedValue, + onChanged: onChanged, + direction: direction, + spacing: spacing, + ), + if (!isEdit) + Text( + text.isNotEmpty ? text : (selectedValue ?? ''), + style: TextStyle(fontSize: 13), + ), + ], + ), + ), + ); + } + + static Widget _buildOptions({ + required List options, + required String? selectedValue, + required ValueChanged onChanged, + Axis direction = Axis.horizontal, + double spacing = 8.0, + }) { + final children = options.map((option) { + return GestureDetector( + onTap: () => onChanged(option), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: selectedValue == option ? Colors.blue : Colors.grey, + width: 2, + ), + color: selectedValue == option ? Colors.blue : Colors.transparent, + ), + child: selectedValue == option + ? Icon( + Icons.check, + size: 12, + color: Colors.white, + ) + : null, + ), + SizedBox(width: 4), + Text( + option, + style: TextStyle( + fontSize: 13, + color: Colors.black, + ), + ), + ], + ), + ); + }).toList(); + + if (direction == Axis.horizontal) { + return Wrap( + spacing: spacing, + children: children, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children + .map((child) => Padding( + padding: EdgeInsets.only(bottom: spacing / 2), + child: child, + )) + .toList(), + ); + } + } + + /// 列表标题头(蓝色标识+文字) + static Widget createBuildSimpleSection( + String title, { + double horPadding = 10, + Color color = Colors.white, + }) { + return Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horPadding, vertical: 10), + child: Row( + children: [ + Container(width: 3, height: 15, color: Colors.blue), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ); + } + + /// 列表标题头(蓝色圆点+文字) + static Widget createBlueDotSection( + String title, + {double horPadding = 10, + int color=0xFFf1f1f1}) { + return Container( + decoration: BoxDecoration( + color: Color(color), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horPadding, vertical: 10), + child: Row( + children: [ + Container( + width: 7, + height: 7, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(180), + ), + ), + const SizedBox(width: 8), + Expanded( + // 添加 Expanded + child: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + } + + /// 单纯标题 + static Widget headerTitle(String title, {bool isRequired = false, double verticalPadding = 0, + double horizontalPadding = 0,}) { + return Container( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: verticalPadding), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + if (isRequired) Text('* ', style: TextStyle(color: Colors.red)), + Text( + title, + maxLines: 5, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ) + ], + ), + ); + } + + /// 扩展项(根据需求自定义) + static Widget createCustomItem({ + required Widget child, + double verticalPadding = 15, + double horizontalPadding = 0, + }) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + child: child, + ); + } + + /// 标题加输入框上下排列 + static Widget createBuildMultilineInput( + String label, + String hint, + TextEditingController controller, { + bool isRequired = false, + int inputNum=120, + }) { + return Container( + height: 130, + padding: const EdgeInsets.only(top: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isRequired) Text('* ', style: TextStyle(color: Colors.red)), + // 标题 + Expanded( + child:HhTextStyleUtils.mainTitle(label, fontSize: 13), + ), + ], + ), + + const SizedBox(height: 8), + // 文本输入框,清除默认内边距以与标题左对齐 + Expanded( + child: TextField( + autofocus: false, + controller: controller, + maxLength: inputNum, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + hintText: hint, + border: InputBorder.none, + contentPadding: + isRequired + ? EdgeInsets.symmetric(horizontal: 10) + : EdgeInsets.zero, // 去除默认内边距 + ), + ), + ), + ], + ), + ); + } + + /// 分类头部 + static Widget createYesNoSectionTwo({ + required String title, + required String yesLabel, + required String noLabel, + required bool groupValue, + required bool canClick, + required BuildContext context, + required ValueChanged onChanged, + double verticalPadding = 15, + double horizontalPadding = 10, + }) { + return Padding( + padding: EdgeInsets.only( + top: 0, + right: horizontalPadding, + left: horizontalPadding, + bottom: verticalPadding, + ), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + Row( + children: [ + Row( + children: [ + Radio( + activeColor: Colors.blue, + value: true, + groupValue: groupValue, + onChanged: (val) { + if (canClick) { + onChanged(val!); + } else { + ToastUtil.showNormal(context, "重大隐患不允许选此项"); + } + }, + // (val) => onChanged(val!), + ), + Text(yesLabel), + ], + ), + const SizedBox(width: 16), + Row( + children: [ + Radio( + activeColor: Colors.blue, + value: false, + groupValue: groupValue, + onChanged: (val) => onChanged(val!), + ), + Text(noLabel), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/customWidget/MultiDictValuesPicker.dart b/lib/customWidget/MultiDictValuesPicker.dart new file mode 100644 index 0000000..5374165 --- /dev/null +++ b/lib/customWidget/MultiDictValuesPicker.dart @@ -0,0 +1,475 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; + +// 数据字典模型 +class DictCategory { + final String id; + final String name; + final Map extValues; + final String parentId; + final String dictValue; + final String dictLabel; + final List children; + + late final String dingValue; + late final String dingName; + + DictCategory({ + required this.id, + required this.name, + required this.children, + required this.extValues, + 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) { + try { + childrenList = rawChildren + .where((e) => e != null) + .map((e) => DictCategory.fromJson(Map.from(e as Map))) + .toList(); + } catch (e) { + childrenList = []; + } + } + + // 处理扩展值 + final extRaw = json['extValues']; + Map extMap = {}; + if (extRaw is Map) { + extMap = Map.from(extRaw); + } + + return DictCategory( + id: parseString(json['id']), + name: parseString(json['dictLabel'] ?? json['name']), // 兼容新旧字段 + children: childrenList, + extValues: extMap, + parentId: parseString(json['parentId']), + dictValue: parseString(json['dictValue']), + dictLabel: parseString(json['dictLabel'] ?? json['name']), + dingValue: "", + dingName: "", + ); + } + + // 转换为Map,便于使用 + Map toMap() { + return { + 'id': id, + 'name': name, + 'dictValue': dictValue, + 'dictLabel': dictLabel, + 'parentId': parentId, + 'extValues': extValues, + }; + } +} + +/// 数据字典选择器回调签名 +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; + + const MultiDictValuesPicker({ + Key? key, + required this.dictType, + required this.onSelected, + this.showSearch = true, + this.title = '请选择', + this.confirmText = '确定', + this.cancelText = '取消', + }) : super(key: key); + + @override + _MultiDictValuesPickerState createState() => _MultiDictValuesPickerState(); +} + +class _MultiDictValuesPickerState extends State { + String selectedId = ''; + String selectedName = ''; + Map? selectedExtraData; + Set expandedSet = {}; + + List original = []; + List filtered = []; + bool loading = true; + bool error = false; + String errorMessage = ''; + + String dingValueWai = ''; + String dingNameWai = ''; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + selectedId = ''; + selectedName = ''; + expandedSet = {}; + _searchController.addListener(_onSearchChanged); + _loadDictData(); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + Future _loadDictData() async { + try { + setState(() { + loading = true; + error = false; + }); + + final result = await BasicInfoApi.getDictValues(widget.dictType); + final raw = result['data'] as List; + setState(() { + original = raw.map((e) => DictCategory.fromJson(e as Map)).toList(); + filtered = original; + loading = false; + }); + } catch (e) { + setState(() { + loading = false; + error = true; + errorMessage = e.toString(); + }); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase().trim(); + setState(() { + filtered = query.isEmpty ? original : _filterCategories(original, query); + // 搜索时展开所有节点以便查看结果 + if (query.isNotEmpty) { + expandedSet.addAll(_getAllExpandableIds(filtered)); + } + }); + } + + Set _getAllExpandableIds(List categories) { + Set ids = {}; + for (var category in categories) { + if (category.children.isNotEmpty) { + ids.add(category.id); + ids.addAll(_getAllExpandableIds(category.children)); + } + } + return ids; + } + + List _filterCategories(List list, String query) { + List result = []; + for (var cat in list) { + final children = _filterCategories(cat.children, query); + if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) { + result.add( + DictCategory( + id: cat.id, + name: cat.name, + children: children, + extValues: cat.extValues, + parentId: cat.parentId, + dictValue: cat.dictValue, + dictLabel: cat.dictLabel, + dingValue: "", + dingName: "", + ), + ); + } + } + return result; + } + + Widget _buildRow(DictCategory category, int indent) { + final hasChildren = category.children.isNotEmpty; + final isExpanded = expandedSet.contains(category.id); + final isSelected = category.id == selectedId; + + return Column( + children: [ + InkWell( + onTap: () { + setState(() { + if (hasChildren) { + 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; + } + }); + }, + child: Container( + color: Colors.white, + child: Row( + children: [ + SizedBox(width: 16.0 * indent), + SizedBox( + width: 24, + child: hasChildren + ? Icon( + isExpanded + ? Icons.arrow_drop_down_rounded + : Icons.arrow_right_rounded, + size: 35, + color: Colors.grey[600], + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: Colors.black, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: Colors.blue, + ), + ), + ], + ), + ), + ), + if (hasChildren && isExpanded) + ...category.children.map((child) => _buildRow(child, indent + 1)), + ], + ); + } + + Widget _buildTitleBar() { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Center( + child: Text( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + Widget _buildActionBar() { + return Container( + color: Colors.white, + 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), + ), + ), + ), + + // 搜索框(如果有搜索功能) + if (widget.showSearch) ...[ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SearchBarWidget( + controller: _searchController, + isShowSearchButton: false, + onSearch: (keyboard) {}, + ), + ), + ), + ] else ...[ + const Expanded(child: SizedBox()), + ], + + // 确定按钮 + GestureDetector( + onTap: selectedId.isEmpty + ? null + : () { + selectedExtraData?['dingValue']=dingValueWai ; + selectedExtraData?['dingName']=dingNameWai ; + Navigator.of(context).pop(); + widget.onSelected(selectedId, selectedName, selectedExtraData); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + widget.confirmText, + style: TextStyle( + fontSize: 16, + color: selectedId.isEmpty ? Colors.grey : Colors.blue, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildContent() { + if (loading) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('加载中...'), + ], + ), + ); + } + + if (error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 48, color: Colors.red), + 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, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadDictData, + child: const Text('重试'), + ), + ], + ), + ); + } + + if (filtered.isEmpty) { + 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)), + ], + ), + ); + } + + return Container( + color: Colors.white, + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + 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), + ), + ], + ), + child: Column( + children: [ + // 标题行 + // _buildTitleBar(), + // 操作行(取消、搜索、确定) + _buildActionBar(), + const Divider(height: 1), + + // 内容区域 + Expanded( + child: _buildContent(), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/big_video_viewer.dart b/lib/customWidget/big_video_viewer.dart new file mode 100644 index 0000000..68d0000 --- /dev/null +++ b/lib/customWidget/big_video_viewer.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:qhd_prevention/customWidget/video_player_widget.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:video_player/video_player.dart'; +// 查看视频 +class BigVideoViewer extends StatefulWidget { + const BigVideoViewer({Key? key, required this.videoUrl}) : super(key: key); + + final String videoUrl; + + @override + State createState() => _BigVideoViewerState(); +} + +class _BigVideoViewerState extends State { + + VideoPlayerController? _videoController; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _videoController?.removeListener(_controllerListener); + _videoController?.dispose(); + _videoController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) + ..initialize().then((_) { + _videoController! + ..play() + ..addListener(_controllerListener); + }); + } + + void _controllerListener() { + if (mounted) setState(() {}); + } + + @override + void dispose() { + _videoController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + + return Scaffold( + backgroundColor: Colors.black, + appBar: MyAppbar( + backgroundColor: Colors.transparent, title: '', + ), + body: SafeArea(child: Center( + child: VideoPlayerWidget( + allowSeek: false, + controller: _videoController, + coverUrl:"", + aspectRatio: _videoController?.value.aspectRatio ?? 16/9, + ), + ),) + ); + } +} diff --git a/lib/customWidget/bottom_picker.dart b/lib/customWidget/bottom_picker.dart new file mode 100644 index 0000000..1dc38c5 --- /dev/null +++ b/lib/customWidget/bottom_picker.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; + +/// 通用底部弹窗选择器 +/// Example: +/// ```dart +/// final choice = await BottomPicker.show( +/// context, +/// items: ['选项1', '选项2', '选项3'], +/// itemBuilder: (item) => Text(item, textAlign: TextAlign.center), +/// initialIndex: 1, +/// ); +/// if (choice != null) { +/// // 用户点击确定并选择了 choice +/// } +/// ``` +class BottomPicker { + /// 显示底部选择器弹窗 + /// + /// [items]: 选项列表 + /// [itemBuilder]: 每个选项的展示 Widget + /// [initialIndex]: 初始选中索引 + /// [itemExtent]: 列表行高 + /// [height]: 弹窗总高度 + static Future show( + BuildContext context, { + required List items, + required Widget Function(T item) itemBuilder, + int initialIndex = 0, + double itemExtent = 50.0, + double height = 250, + }) { + if (items.isEmpty) return Future.value(null); + + // 确保初始索引合法 + final safeIndex = initialIndex.clamp(0, items.length - 1); + // 当前选中项 + T selected = items[safeIndex]; + + return showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (ctx) { + return SizedBox( + height: height, + child: Column( + children: [ + // 按钮行 + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + FocusScope.of(context).unfocus(); + Navigator.of(ctx).pop(); + }, + child: const Text('取消', style: TextStyle(color: Colors.black54, fontSize: 16),), + ), + TextButton( + onPressed: () { + FocusScope.of(context).unfocus(); + Navigator.of(ctx).pop(selected); + }, + child: const Text('确定', style: TextStyle(color: Colors.blue, fontSize: 16),), + ), + ], + ), + ), + const Divider(height: 1), + // 滚动选择器 + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: initialIndex, + ), + itemExtent: itemExtent, + onSelectedItemChanged: (index) { + selected = items[index]; + }, + // 把 itemBuilder 返回的 Widget 用 Center 包一层 + children: items.map((item) => Center(child: itemBuilder(item))).toList(), + ), + ), + + ], + ), + ); + }, + ); + } +} diff --git a/lib/customWidget/bottom_picker_two.dart b/lib/customWidget/bottom_picker_two.dart new file mode 100644 index 0000000..5467b37 --- /dev/null +++ b/lib/customWidget/bottom_picker_two.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +/// 通用底部弹窗选择器 +/// Example: +/// ```dart +/// final choice = await BottomPickerTwo.show( +/// context, +/// items: ['选项1', '选项2', '选项3'], +/// itemBuilder: (item) => Text(item, textAlign: TextAlign.center), +/// initialIndex: 1, +/// ); +/// if (choice != null) { +/// // 用户点击确定并选择了 choice +/// } +/// ``` +class BottomPickerTwo { + /// 显示底部选择器弹窗 + /// + /// [items]: 选项列表 + /// [itemBuilder]: 每个选项的展示 Widget + /// [initialIndex]: 初始选中索引 + /// [itemExtent]: 列表行高 + /// [height]: 弹窗总高度 + static Future show( + BuildContext context, { + required List items, + required Widget Function(dynamic item) itemBuilder, + int initialIndex = 0, + double itemExtent = 50.0, + double height = 250, + double desiredSpacing = 16.0, + String itemName='name', + }) { + // 当前选中项 + dynamic selected = items[initialIndex]; + + return showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (ctx) { + return SizedBox( + height: height, + child: Column( + children: [ + // 按钮行 + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('取消'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(selected[itemName]), + child: const Text('确定'), + ), + ], + ), + ), + const Divider(height: 1), + // 滚动选择器 + Expanded( + child: CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: initialIndex, + ), + itemExtent: itemExtent, + onSelectedItemChanged: (index) { + selected = items[index]; + }, + // children: items.map(itemBuilder).toList(), + children: List.generate(items.length, (int index) { + return Padding( + // 通过Padding调整项间距 + padding: EdgeInsets.symmetric( + vertical: desiredSpacing / 2, + ), + child: Center( + child: Text( + // '选项 $index', + items[index][itemName], + style: TextStyle( + fontSize: 15, + color: + index == selected + ? CupertinoColors.activeBlue + : CupertinoColors.black, + fontWeight: + index == selected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ), + ); + }), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/customWidget/center_multi_picker.dart b/lib/customWidget/center_multi_picker.dart new file mode 100644 index 0000000..718c555 --- /dev/null +++ b/lib/customWidget/center_multi_picker.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; + +/// 居中多选弹窗(Dialog) +/// 返回 Future?>:用户点击确定返回所选项列表;取消或关闭返回 null。 +class CenterMultiPicker { + static Future?> show( + BuildContext context, { + required List items, + required Widget Function(T item) itemBuilder, + List? initialSelectedIndices, + int? maxSelection, + bool allowEmpty = false, + double itemHeight = 52, + double maxHeightFactor = 0.75, // 屏幕高度的最大占比 + String? title, + }) { + if (items.isEmpty) return Future.value(null); + + // 安全化初始索引 + final initialSet = {}; + if (initialSelectedIndices != null) { + for (final i in initialSelectedIndices) { + // if (i >= 0 && i < items.length) initialSet.add(i); + } + } + + return showDialog?>( + context: context, + barrierDismissible: true, + builder: (ctx) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + insetPadding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 24, + ), + child: _CenterMultiPickerBody( + items: items, + itemBuilder: itemBuilder, + initialSelected: initialSet, + maxSelection: maxSelection, + allowEmpty: allowEmpty, + itemHeight: itemHeight, + maxHeightFactor: maxHeightFactor, + title: title, + ), + ); + }, + ); + } +} + +class _CenterMultiPickerBody extends StatefulWidget { + const _CenterMultiPickerBody({ + Key? key, + required this.items, + required this.itemBuilder, + required this.initialSelected, + required this.maxSelection, + required this.allowEmpty, + required this.itemHeight, + required this.maxHeightFactor, + this.title, + }) : super(key: key); + + final List items; + final Widget Function(T item) itemBuilder; + final Set initialSelected; + final int? maxSelection; + final bool allowEmpty; + final double itemHeight; + final double maxHeightFactor; + final String? title; + + @override + State<_CenterMultiPickerBody> createState() => + _CenterMultiPickerBodyState(); +} + +class _CenterMultiPickerBodyState extends State<_CenterMultiPickerBody> { + late Set _selected; + + // 固定的 header / footer 高度估算 + static const double _headerHeight = 56; + static const double _footerHeight = 58; + static const double _verticalPadding = 16; // Dialog 内上下 padding + + @override + void initState() { + super.initState(); + _selected = Set.from(widget.initialSelected); + } + + void _toggle(int idx) { + setState(() { + if (_selected.contains(idx)) { + _selected.remove(idx); + } else { + if (widget.maxSelection != null && + _selected.length >= widget.maxSelection!) { + ToastUtil.showNormal(context, '最多可选择 ${widget.maxSelection} 项'); + return; + } + _selected.add(idx); + } + }); + } + + @override + Widget build(BuildContext context) { + final items = widget.items; + final screenH = MediaQuery.of(context).size.height; + final contentHeight = + items.length * widget.itemHeight + + _headerHeight + + _footerHeight + + _verticalPadding * 2; + final maxAllowed = screenH * widget.maxHeightFactor; + final dialogHeight = + contentHeight <= maxAllowed ? contentHeight : maxAllowed; + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10) + ), + width: double.infinity, + height: dialogHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // header + Container( + height: _headerHeight, + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Row( + children: [ + if (widget.title != null) + Expanded( + child: Text( + widget.title!, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ) + else + Expanded( + child: Text( + '已选 ${_selected.length}${widget.maxSelection != null ? '/${widget.maxSelection}' : ''}', + style: const TextStyle(fontSize: 15), + ), + ), + // 可将一些快捷按钮放在右侧(如全选/反选),这里暂不显示 + ], + ), + ), + const Divider(height: 1), + // 列表区域(可滚动) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Scrollbar( + thumbVisibility: true, + child: ListView.separated( + physics: const BouncingScrollPhysics(), + itemCount: items.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (ctx, idx) { + final isSelected = _selected.contains(idx); + return InkWell( + onTap: () => _toggle(idx), + child: Container( + height: widget.itemHeight, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 6, + ), + child: Row( + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: + isSelected + ? Colors.blue + : Colors.transparent, + border: Border.all( + color: + isSelected ? Colors.blue : Colors.black26, + ), + borderRadius: BorderRadius.circular(4), + ), + child: + isSelected + ? const Icon( + Icons.check, + size: 18, + color: Colors.white, + ) + : null, + ), + const SizedBox(width: 12), + Expanded(child: widget.itemBuilder(items[idx])), + ], + ), + ), + ); + }, + ), + ), + ), + ), + const Divider(height: 1), + // footer: 取消 / 确定(固定在底部) + Container( + height: _footerHeight, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Expanded( + child: CustomButton( + text: '取消', + backgroundColor: Colors.grey.shade200, + textStyle: TextStyle(fontSize: 14, color: Colors.black), + onPressed: () { + Navigator.of(context).pop(null); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: CustomButton( + text: '确定', + backgroundColor: Colors.blue, + onPressed: () { + if (!widget.allowEmpty && _selected.isEmpty) { + ToastUtil.showNormal(context, '请至少选择一项'); + return; + } + final result = _selected + .map((i) => items[i]) + .toList(growable: false); + Navigator.of(context).pop(result); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/customWidget/custom_alert_dialog.dart b/lib/customWidget/custom_alert_dialog.dart new file mode 100644 index 0000000..ad5c9ca --- /dev/null +++ b/lib/customWidget/custom_alert_dialog.dart @@ -0,0 +1,301 @@ +// custom_alert_dialog.dart +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/main.dart'; // 导入全局 navigatorKey + +/// 对话框模式 +enum DialogMode { text, input } + +class CustomAlertDialog extends StatefulWidget { + final String title; + final String content; + final String hintText; + final String cancelText; + final String confirmText; + final VoidCallback? onCancel; + final VoidCallback? onConfirm; + final ValueChanged? onInputConfirm; + final DialogMode mode; + final bool force; + + const CustomAlertDialog({ + Key? key, + required this.title, + this.content = '', + this.hintText = '', + this.cancelText = '取消', + this.confirmText = '确定', + this.onCancel, + this.onConfirm, + this.onInputConfirm, + this.mode = DialogMode.text, + this.force = false, + }) : super(key: key); + + // ------------------ 静态快捷方法 ------------------ + + static Future showConfirm( + BuildContext context, { + required String title, + String content = '', + String cancelText = '取消', + String confirmText = '确定', + bool barrierDismissible = true, + VoidCallback? onConfirm, + bool force = false, + }) async { + final result = await showDialog( + context: context, + barrierDismissible: force ? false : barrierDismissible, + builder: (_) => PopScope( + canPop: false, + child: CustomAlertDialog( + title: title, + content: content, + cancelText: cancelText, + confirmText: confirmText, + onConfirm: onConfirm, + force: force, + ),) + ); + return result == true; + } + + static Future showAlert( + BuildContext context, { + required String title, + String content = '', + String confirmText = '确定', + bool barrierDismissible = true, + VoidCallback? onConfirm, + bool force = false, + }) async { + await showDialog( + context: context, + barrierDismissible: force ? false : barrierDismissible, + builder: (_) => CustomAlertDialog( + title: title, + content: content, + cancelText: '', + confirmText: confirmText, + onConfirm: onConfirm, + force: force, + ), + ); + } + + static Future showInput( + BuildContext context, { + required String title, + String hintText = '', + String cancelText = '取消', + String confirmText = '确定', + bool barrierDismissible = true, + bool force = false, + }) async { + final result = await showDialog( + context: context, + barrierDismissible: force ? false : barrierDismissible, + builder: (_) => CustomAlertDialog( + title: title, + hintText: hintText, + cancelText: cancelText, + confirmText: confirmText, + mode: DialogMode.input, + force: force, + ), + ); + // 取消/点遮罩会得到 null;确认会得到 String(可能为空串) + return result; + } + + + @override + _CustomAlertDialogState createState() => _CustomAlertDialogState(); +} + +class _CustomAlertDialogState extends State { + late TextEditingController _controller; + bool _isClosing = false; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get hasCancel => !widget.force && widget.cancelText.isNotEmpty; + + void _closeDialog([dynamic result]) { + if (_isClosing) return; + _isClosing = true; + + // 优先使用当前上下文导航 + if (mounted) { + Navigator.of(context).pop(result); + } else { + // 后备方案:使用全局导航键 + navigatorKey.currentState?.pop(result); + } + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: !widget.force, + child: Dialog( + backgroundColor: Colors.transparent, + child: Container( + constraints: const BoxConstraints(minWidth: 280), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + widget.title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 16), + if (widget.mode == DialogMode.text) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + widget.content, + style: const TextStyle( + fontSize: 16, + color: Colors.black54, + ), + textAlign: TextAlign.center, + ), + ) + else + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _controller, + autofocus: true, + decoration: InputDecoration( + hintText: widget.hintText, + border: const OutlineInputBorder(), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.blue, width: 1), + borderRadius: BorderRadius.circular(4), + ), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + ), + ), + ), + const SizedBox(height: 20), + const Divider(height: 1), + hasCancel + ? _buildDoubleButtons(context) + : _buildSingleButton(context), + ], + ), + ), + ), + ); + } + + Widget _buildDoubleButtons(BuildContext context) { + return Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + widget.onCancel?.call(); + _closeDialog(widget.mode == DialogMode.text ? false : null); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Text( + widget.cancelText, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Colors.black87, + fontSize: 18, + ), + ), + ), + ), + ), + Container(width: 1, height: 48, color: Colors.grey[300]), + Expanded( + child: InkWell( + onTap: () { + if (widget.mode == DialogMode.text) { + widget.onConfirm?.call(); + _closeDialog(true); + } else { + final value = _controller.text.trim(); + widget.onInputConfirm?.call(value); + _closeDialog(value); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: Alignment.center, + child: Text( + widget.confirmText, + style: const TextStyle( + color: Color(0xFF1C61FF), + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ), + ), + ], + ); + } + + Widget _buildSingleButton(BuildContext context) { + return InkWell( + onTap: () { + if (widget.mode == DialogMode.text) { + widget.onConfirm?.call(); + _closeDialog(true); + } else { + final value = _controller.text.trim(); + widget.onInputConfirm?.call(value); + _closeDialog(value); + } + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 14), + alignment: Alignment.center, + child: Text( + widget.confirmText, + style: const TextStyle( + color: Color(0xFF1C61FF), + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/custom_button.dart b/lib/customWidget/custom_button.dart new file mode 100644 index 0000000..fd145e2 --- /dev/null +++ b/lib/customWidget/custom_button.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; + +/// 按钮样式类型 +enum ButtonStyleType { + primary, // 主样式:纯色背景,无边框 + secondary, // 次样式:白色背景,深灰文字,深灰边框 +} + +/// 自定义默认按钮(支持不可点击/禁用状态和防连点功能) +class CustomButton extends StatefulWidget { + final String text; // 按钮文字 + final Color backgroundColor; // 按钮背景色 + final double borderRadius; // 圆角半径(默认5) + final VoidCallback? onPressed; // 点击事件回调 + final EdgeInsetsGeometry? padding; // 内边距 + final EdgeInsetsGeometry? margin; // 外边距 + final double? height; // 按钮高度 + final TextStyle? textStyle; // 文字样式 + + /// 新增:是否可点击(true 可点,false 禁用) + /// 注意:如果 onPressed 为 null,也会被视为不可点击 + final bool enabled; + + /// 新增:禁用时的背景色(可选) + final Color? disabledBackgroundColor; + + /// 新增:禁用时的文字颜色(可选) + final Color? disabledTextColor; + + /// 新增:文字颜色(可选) + final Color? textColor; + + /// 新增:防连点间隔时间(毫秒) + final int debounceInterval; + + /// 新增:按钮样式类型 + final ButtonStyleType buttonStyle; + + /// 新增:边框颜色(仅在 secondary 样式下使用,如不指定则使用默认深灰色) + final Color? borderColor; + + /// 新增:边框宽度(仅在 secondary 样式下使用) + final double borderWidth; + + const CustomButton({ + super.key, + required this.text, + this.backgroundColor = Colors.blue, + this.borderRadius = 5.0, + this.onPressed, + this.padding, + this.margin, + this.height, + this.textStyle, + this.textColor, + this.enabled = true, + this.disabledBackgroundColor, + this.disabledTextColor, + this.debounceInterval = 1000, // 默认1秒防连点 + this.buttonStyle = ButtonStyleType.primary, // 默认为主样式 + this.borderColor, + this.borderWidth = 1.0, + }); + + @override + State createState() => _CustomButtonState(); +} + +class _CustomButtonState extends State { + // 记录最后一次点击时间 + DateTime _lastClickTime = DateTime(0); + + @override + Widget build(BuildContext context) { + // 如果 enabled 为 false 或 onPressed 为 null,则视为不可点击 + final bool isEnabled = widget.enabled && widget.onPressed != null; + + // 根据按钮样式计算背景色、文字颜色和边框 + final Color bgColor; + final Color textColor; + final Color? borderColor; + + if (widget.buttonStyle == ButtonStyleType.secondary) { + // 次样式:白色背景,深灰文字,深灰边框 + bgColor = isEnabled + ? Colors.white + : (widget.disabledBackgroundColor ?? Colors.grey.shade200); + + textColor = isEnabled + ? Colors.grey[800]! + : (widget.disabledTextColor ?? Colors.grey.shade500); + + borderColor = isEnabled + ? (widget.borderColor ?? Colors.grey.shade400) + : (widget.disabledTextColor ?? Colors.grey.shade300); + } else { + // 主样式:原有逻辑 + bgColor = isEnabled + ? widget.backgroundColor + : (widget.disabledBackgroundColor ?? Colors.grey.shade400); + + textColor = widget.textColor ?? (isEnabled ? Colors.white : (widget + .disabledTextColor ?? Colors.white70)); + borderColor = null; // 主样式默认无边框 + } + + // 计算最终文字样式 + TextStyle finalTextStyle; + if (widget.textStyle != null) { + finalTextStyle = widget.textStyle!.copyWith(color: textColor); + } else { + finalTextStyle = TextStyle( + color: textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ); + } + + // 处理点击事件(添加防连点逻辑) + void handleOnPressed() { + final now = DateTime.now(); + if (now + .difference(_lastClickTime) + .inMilliseconds < widget.debounceInterval) { + // 在防连点间隔内,不执行操作 + return; + } + + _lastClickTime = now; + + if (widget.onPressed != null) { + widget.onPressed!(); + } + } + + // 构建边框 + final BoxBorder? border = borderColor != null + ? Border.all( + color: borderColor, + width: widget.borderWidth, + ) + : null; + + // 点击拦截器 + 视觉反馈(禁用时降低不透明度) + return Opacity( + opacity: isEnabled ? 1.0 : 0.65, + child: AbsorbPointer( + absorbing: !isEnabled, + child: GestureDetector( + onTap: isEnabled ? handleOnPressed : null, + child: Container( + height: widget.height ?? 45, + // 默认高度45 + padding: widget.padding ?? const EdgeInsets.all(6), + // 默认内边距 + margin: widget.margin ?? const EdgeInsets.symmetric(horizontal: 5), + // 默认外边距 + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.borderRadius), + color: bgColor, + border: border, + ), + child: Center( + child: Text( + widget.text, + style: finalTextStyle, + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/danner_repain_item.dart b/lib/customWidget/danner_repain_item.dart new file mode 100644 index 0000000..84071f0 --- /dev/null +++ b/lib/customWidget/danner_repain_item.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import '../tools/tools.dart'; + +/// 通用列表卡片组件: +/// - 两两为一组,固定左右布局,平均分配宽度 +/// - 左侧文字左对齐,右侧文字右对齐 +/// - 文字过多时各自自动换行 +/// - 如果一行只有一个元素,靠左显示 +class DannerRepainItem extends StatelessWidget { + final String title; + final List details; + final bool showBottomTags; + final List bottomTags; + final bool showTitleIcon; + final VoidCallback? onPressed; + final bool showBottomBtn; + final String showBottomText; + + const DannerRepainItem({ + Key? key, + required this.title, + required this.details, + this.showBottomTags = false, + this.showTitleIcon = true, + this.bottomTags = const [], + this.onPressed, + this.showBottomBtn = true, + this.showBottomText = '查看', + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 15, right: 15, bottom: 15), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // — 标题行 — + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + if (showTitleIcon) + const Icon(Icons.star_rate_sharp, color: Colors.blue, size: 18), + SizedBox(width: showTitleIcon ? 5 : 0), + Text(title, style: const TextStyle(fontSize: 14)), + ]), + const Icon(Icons.arrow_forward_ios_rounded, color: Colors.grey, size: 15), + ], + ), + ), + const Divider(height: 1), + + // — 详情区:固定左右布局 — + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Column( + children: _buildDetailRows(), + ), + ), + + // — 底部标签区 — + if (showBottomTags && bottomTags.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 15, right: 15, bottom: 10), + child: Wrap(spacing: 5, runSpacing: 5, children: bottomTags), + ), + + if (showBottomBtn) + CustomButton(text: showBottomText,backgroundColor: Colors.blue,margin: EdgeInsets.symmetric(horizontal: 10), + onPressed: onPressed,), + SizedBox(height: 5,), + ], + ), + ), + ); + } + + // 构建详情行 + List _buildDetailRows() { + List rows = []; + for (int i = 0; i < details.length; i += 2) { + final left = details[i]; + final right = (i + 1 < details.length) ? details[i + 1] : ''; + + rows.add( + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _DetailRow(left: left, right: right), + ), + ); + } + return rows; + } +} + +/// 详情行组件:固定左右布局,平均分配宽度,各自换行 +/// 如果只有左侧文本,则靠左显示 +class _DetailRow extends StatelessWidget { + final String left; + final String right; + + const _DetailRow({ + Key? key, + required this.left, + required this.right, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + // 如果右侧文本为空,只显示左侧文本,靠左显示 + if (right.isEmpty) { + return Align( + alignment: Alignment.centerLeft, + child: _DetailText(left, textAlign: TextAlign.left), + ); + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧文本 - 占50%宽度 + Expanded( + flex: 1, + child: _DetailText(left, textAlign: TextAlign.left), + ), + + // 中间间距 + const SizedBox(width: 10), + + // 右侧文本 - 占50%宽度 + Expanded( + flex: 1, + child: _DetailText(right, textAlign: TextAlign.right), + ), + ], + ); + } +} + +/// Detail 文本封装:支持自动换行和指定的对齐方式 +class _DetailText extends StatelessWidget { + final String text; + final TextAlign textAlign; + + const _DetailText(this.text, { + Key? key, + required this.textAlign, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Text( + text, + style: HhTextStyleUtils.secondaryTitleStyle, + softWrap: true, // 允许换行 + textAlign: textAlign, // 使用指定的对齐方式 + ); + } +} \ No newline at end of file diff --git a/lib/customWidget/date_picker_dialog.dart b/lib/customWidget/date_picker_dialog.dart new file mode 100644 index 0000000..057d4bb --- /dev/null +++ b/lib/customWidget/date_picker_dialog.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; + +class HDatePickerDialog extends StatefulWidget { + final DateTime initialDate; + final VoidCallback onCancel; + final ValueChanged onConfirm; + + const HDatePickerDialog({ + Key? key, + required this.initialDate, + required this.onCancel, + required this.onConfirm, + }) : super(key: key); + + @override + _HDatePickerDialogState createState() => _HDatePickerDialogState(); +} + +class _HDatePickerDialogState extends State { + late DateTime _displayedMonth; + late DateTime _selectedDate; + + @override + void initState() { + super.initState(); + _selectedDate = widget.initialDate; + _displayedMonth = DateTime(widget.initialDate.year, widget.initialDate.month); + } + + void _prevMonth() { + setState(() { + _displayedMonth = DateTime(_displayedMonth.year, _displayedMonth.month - 1); + }); + } + + void _nextMonth() { + setState(() { + _displayedMonth = DateTime(_displayedMonth.year, _displayedMonth.month + 1); + }); + } + + @override + Widget build(BuildContext context) { + final daysInMonth = DateUtils.getDaysInMonth(_displayedMonth.year, _displayedMonth.month); + final firstWeekday = DateTime(_displayedMonth.year, _displayedMonth.month, 1).weekday; + + return GestureDetector( + onTap: widget.onCancel, + child: Material( + color: Colors.black54, + child: Center( + child: GestureDetector( + onTap: () {}, // 拦截点击 + child: Container( + width: 320, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 顶部显示选中日期 + Text( + DateFormat('yyyy-MM-dd').format(_selectedDate), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + + // 月份选择 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: _prevMonth, + ), + Text( + DateFormat('yyyy 年 MM 月').format(_displayedMonth), + style: const TextStyle(fontSize: 16), + ), + IconButton( + icon: const Icon(Icons.chevron_right), + onPressed: _nextMonth, + ), + ], + ), + + // 星期标题 + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: ['一', '二', '三', '四', '五', '六', '日'] + .map((d) => Expanded( + child: Center(child: Text(d)), + )) + .toList(), + ), + SizedBox(height: 10,), + // 日历网格 + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 7, + childAspectRatio: 1.2, + crossAxisSpacing: 4, + mainAxisSpacing: 4, + ), + itemCount: daysInMonth + firstWeekday - 1, + itemBuilder: (context, index) { + if (index < firstWeekday - 1) { + return const SizedBox(); + } + final day = index - firstWeekday + 2; + final date = DateTime(_displayedMonth.year, _displayedMonth.month, day); + final isSelected = DateUtils.isSameDay(date, _selectedDate); + + return GestureDetector( + onTap: () { + setState(() { + _selectedDate = date; + }); + }, + child: Container( + decoration: BoxDecoration( + color: isSelected ? Colors.blue : null, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + '$day', + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + ), + ), + ), + ), + ); + }, + ), + + const SizedBox(height: 16), + + // 按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded(child: CustomButton(text: '取消',height: 40,backgroundColor: Colors.grey.shade500, + onPressed: widget.onCancel),), + SizedBox(width: 30,), + Expanded(child: CustomButton(text: '确定',height: 40,backgroundColor: Colors.blue, + onPressed: () => widget.onConfirm(_selectedDate)),) + + + ], + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/customWidget/department_person_picker.dart b/lib/customWidget/department_person_picker.dart new file mode 100644 index 0000000..be66b50 --- /dev/null +++ b/lib/customWidget/department_person_picker.dart @@ -0,0 +1,231 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; + +/// 用户数据模型 +class Person { + final String userId; + final String name; + final String departmentName; + final String phone; + + Person({required this.userId, required this.name, required this.departmentName, required this.phone}); + + factory Person.fromJson(Map json) { + return Person( + userId: json['id'] ?? '' , + name: json['username'] ?? '', + departmentName: json['departmentName'] ?? '', + phone: json['phone'] ?? '', + ); + } +} + +/// 原回调签名(向后兼容) +typedef PersonSelectCallback = void Function(String userId, String name); + +/// 新回调签名,增加可选 index(int,默认 0) +typedef PersonSelectCallbackWithIndex = void Function(String userId, String name, int index); + +/// 新增:带 data 的回调(不会替换原有 typedef,向后兼容) +typedef PersonSelectCallbackWithData = void Function(String userId, String name, Map data); + +/// 新增:带 index 和 data 的回调 +typedef PersonSelectCallbackWithIndexAndData = void Function(String userId, String name, int index, Map data); + +/// 底部弹窗人员选择器(使用预先传入的原始数据列表,不做接口请求) +class DepartmentPersonPicker { + /// 显示人员选择弹窗 + /// + /// [personsData]: 已拉取并缓存的原始 Map 列表(每项最好是 Map) + /// [onSelected]: 选中后回调 USER_ID 和 NAME(向后兼容旧代码) + /// [onSelectedWithIndex]: 可选的新回调,额外返回 index(index 为在原始 personsData/_all 中的下标,找不到则为 0) + /// [onSelectedWithData]: 可选回调,返回 userId/name + 选中项的完整原始 Map(不影响旧回调) + /// [onSelectedWithIndexWithData]: 可选回调,返回 userId/name/index + 选中项的完整原始 Map(优先级最高) + static Future show( + BuildContext context, { + required List personsData, + PersonSelectCallback? onSelected, + PersonSelectCallbackWithIndex? onSelectedWithIndex, + PersonSelectCallbackWithData? onSelectedWithData, + PersonSelectCallbackWithIndexAndData? onSelectedWithIndexWithData, + }) async { + // 至少传入一个回调(保持对旧调用的兼容) + assert( + onSelected != null || + onSelectedWithIndex != null || + onSelectedWithData != null || + onSelectedWithIndexWithData != null, + '请至少传入一个回调:onSelected / onSelectedWithIndex / onSelectedWithData / onSelectedWithIndexWithData', + ); + + // 转换为模型(personsData 可能包含非 Map 的条目) + final List _all = personsData.map((e) { + if (e is Map) { + return Person.fromJson(e); + } else { + // 非 map 情况按字符串处理 + final s = e?.toString() ?? ''; + return Person(userId: s, name: s, departmentName: '', phone: ''); + } + }).toList(); + + List _filtered = List.from(_all); + String _selectedName = ''; + String _selectedId = ''; + final TextEditingController _searchController = TextEditingController(); + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: (ctx) { + return StatefulBuilder( + builder: (BuildContext ctx, StateSetter setState) { + // 搜索逻辑 + void _onSearch(String v) { + final q = v.toLowerCase().trim(); + setState(() { + if (q.isEmpty) { + _filtered = List.from(_all); + } else { + _filtered = _all.where((p) { + final nameLower = p.name.toLowerCase(); + final phoneLower = p.phone.toString().toLowerCase(); + return nameLower.contains(q) || phoneLower.contains(q); + }).toList(); + } + }); + } + + // 根据选中的 userId 在原始 personsData 中找到对应的原始 Map(若不存在则生成一个简单 Map) + Map _findOriginalData(String userId) { + try { + final idx = personsData.indexWhere((raw) { + if (raw is Map) { + final id = raw['id']?.toString() ?? raw['userId']?.toString() ?? ''; + return id == userId; + } else { + return raw?.toString() == userId; + } + }); + if (idx >= 0) { + final raw = personsData[idx]; + if (raw is Map) return Map.from(raw); + return {'id': userId, 'username': _selectedName}; + } else { + // 找不到则返回一个最小信息 map + return {'id': userId, 'username': _selectedName}; + } + } catch (e) { + return {'id': userId, 'username': _selectedName}; + } + } + + return SafeArea( + child: SizedBox( + height: MediaQuery.of(ctx).size.height * 0.75, + child: Column( + children: [ + // 顶部:取消、搜索、确定 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text( + '取消', + style: TextStyle(fontSize: 16), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: SearchBarWidget( + controller: _searchController, + onTextChanged: _onSearch, + isShowSearchButton: false, + onSearch: (keyboard) {}, + ), + ), + ), + TextButton( + onPressed: _selectedId.isEmpty + ? null + : () { + Navigator.of(ctx).pop(); + + // 计算 index(在原始 _all 列表中的下标) + final idx = _all.indexWhere((p) => p.userId == _selectedId); + final validIndex = idx >= 0 ? idx : 0; + + // 找到原始 data(Map) + final dataMap = _findOriginalData(_selectedId); + + // 优先调用带 index 和 data 的回调(最高优先) + if (onSelectedWithIndexWithData != null) { + onSelectedWithIndexWithData(_selectedId, _selectedName, validIndex, dataMap); + return; + } + + // 然后是带 data 的回调(只返回 data,不返回 index) + if (onSelectedWithData != null) { + onSelectedWithData(_selectedId, _selectedName, dataMap); + return; + } + + // 然后是带 index 的旧回调 + if (onSelectedWithIndex != null) { + onSelectedWithIndex(_selectedId, _selectedName, validIndex); + return; + } + + // 最后回退到最原始的回调(仅 userId + name) + if (onSelected != null) { + onSelected(_selectedId, _selectedName); + return; + } + }, + child: const Text( + '确定', + style: TextStyle(color: Colors.green, fontSize: 16), + ), + ), + ], + ), + ), + const Divider(height: 1), + // 列表 + Expanded( + child: ListView.separated( + itemCount: _filtered.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final person = _filtered[index]; + final selected = person.userId == _selectedId; + return ListTile( + titleAlignment: ListTileTitleAlignment.center, + title: Text('${person.name}-${person.phone}(${person.departmentName})'), + trailing: selected ? const Icon(Icons.check, color: Colors.green) : null, + onTap: () => setState(() { + _selectedId = person.userId; + _selectedName = person.name; + }), + ); + }, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/customWidget/department_picker.dart b/lib/customWidget/department_picker.dart new file mode 100644 index 0000000..b4b0613 --- /dev/null +++ b/lib/customWidget/department_picker.dart @@ -0,0 +1,270 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import '../tools/tools.dart'; // 包含 SessionService + +// 数据模型 +class Category { + final String id; + final String name; + final Map extValues; + final String departmentId; + final String parentId; + final List childrenList; + + Category({ + required this.id, + required this.name, + required this.childrenList, + required this.extValues, + required this.departmentId, + required this.parentId, + }); + + factory Category.fromJson(Map json) { + // 安全读取并兼容字符串或数字类型的 id + String parseString(dynamic v) { + if (v == null) return ''; + if (v is String) return v; + return v.toString(); + } + + final rawChildren = json['childrenList']; + List children = []; + if (rawChildren is List) { + try { + children = rawChildren + .where((e) => e != null) + .map((e) => Category.fromJson(Map.from(e as Map))) + .toList(); + } catch (e) { + // 如果内部解析出错,保持 children 为空并继续 + children = []; + } + } + + // extValues 有可能为 null 或不是 Map + final extRaw = json['extValues']; + Map extMap = {}; + if (extRaw is Map) { + extMap = Map.from(extRaw); + } + + return Category( + id: parseString(json['id']), + name: parseString(json['name']), + childrenList: children, + extValues: extMap, + departmentId: parseString(json['departmentId']), + parentId: parseString(json['parentId']), + ); + } +} + +/// 弹窗回调签名:返回选中项的 id 和 name +typedef DeptSelectCallback = void Function(String id, String name); + +class DepartmentPicker extends StatefulWidget { + /// 回调,返回选中部门 id 与 name + final DeptSelectCallback onSelected; + + const DepartmentPicker({Key? key, required this.onSelected}) + : super(key: key); + + @override + _DepartmentPickerState createState() => _DepartmentPickerState(); +} + +class _DepartmentPickerState extends State { + String selectedId = ''; + String selectedName = ''; + Set expandedSet = {}; + + List original = []; + List filtered = []; + bool loading = true; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + // 初始均为空 + selectedId = ''; + selectedName = ''; + expandedSet = {}; + _searchController.addListener(_onSearchChanged); + _loadData(); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + Future _loadData() async { + try { + final result = await BasicInfoApi.getDeptTree({}); + final raw = result['data'] as List; + print(raw); + setState(() { + original = raw.map((e) => Category.fromJson(e as Map)).toList(); + filtered = original; + loading = false; + }); + } catch (e) { + setState(() => loading = false); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase().trim(); + setState(() { + filtered = query.isEmpty ? original : _filterCategories(original, query); + }); + } + + List _filterCategories(List list, String query) { + List result = []; + for (var cat in list) { + final children = _filterCategories(cat.childrenList, query); + if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) { + result.add( + Category( + id: cat.id, + name: cat.name, + childrenList: children, + extValues: cat.extValues, + departmentId: cat.departmentId, + parentId: cat.parentId, + ), + ); + } + } + return result; + } + + Widget _buildRow(Category cat, int indent) { + final hasChildren = cat.childrenList.isNotEmpty; + final isExpanded = expandedSet.contains(cat.id); + final isSelected = cat.id == selectedId; + return Column( + children: [ + InkWell( + onTap: () { + setState(() { + if (hasChildren) { + isExpanded + ? expandedSet.remove(cat.id) + : expandedSet.add(cat.id); + } + selectedId = cat.id; + selectedName = cat.name; + }); + }, + child: Container( + color: Colors.white, + child: Row( + children: [ + SizedBox(width: 16.0 * indent), + SizedBox( + width: 24, + child: + hasChildren + ? Icon( + isExpanded + ? Icons.arrow_drop_down_rounded + : Icons.arrow_right_rounded, + size: 35, + color: Colors.grey[600], + ) + : const SizedBox.shrink(), + ), + const SizedBox(width: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text(cat.name), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + color: Colors.blue, + ), + ), + ], + ), + ), + ), + if (hasChildren && isExpanded) + ...cat.childrenList.map((c) => _buildRow(c, indent + 1)), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.7, + color: Colors.white, + child: Column( + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(fontSize: 16)), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SearchBarWidget( + controller: _searchController, + isShowSearchButton: false, + onSearch: (keyboard) {}, + ), + ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + widget.onSelected(selectedId, selectedName); + }, + child: const Text( + '确定', + style: TextStyle(fontSize: 16, color: Colors.blue), + ), + ), + ], + ), + ), + Divider(), + Expanded( + child: + loading + ? const Center(child: CircularProgressIndicator()) + : Container( + color: Colors.white, + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/customWidget/department_picker_three.dart b/lib/customWidget/department_picker_three.dart new file mode 100644 index 0000000..780ba47 --- /dev/null +++ b/lib/customWidget/department_picker_three.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; + + +// 数据模型 +class CategoryTypeThree { + final String id; + final String name; + final String pdId; + final List children; + + CategoryTypeThree({ + required this.id, + required this.name, + required this.pdId, + this.children = const [], + }); + + factory CategoryTypeThree.fromJson(Map json) { + return CategoryTypeThree( + id: json['dataId'] != null ? json['dataId'].toString() : "", + name: json['dataName'] != null ? json['dataName'].toString() : "", + pdId: json['parentId'] != null ? json['parentId'].toString() : "", + children: _parseChildren(json['children']), + ); + } + + static List _parseChildren(dynamic childrenData) { + if (childrenData == null) return []; + if (childrenData is! List) return []; + + return childrenData + .whereType>() + .map((e) => CategoryTypeThree.fromJson(e)) + .toList(); + } + +} + + + +/// 弹窗回调签名:返回选中项的 id 和 name +typedef DeptSelectCallback = void Function(String id, String name,String pdId); + +class DepartmentPickerThree extends StatefulWidget { + /// 回调,返回选中部门 id 与 name + final DeptSelectCallback onSelected; + final listdata; + + const DepartmentPickerThree({Key? key, required this.onSelected, required this.listdata}) : super(key: key); + + @override + _DepartmentPickerThreeState createState() => _DepartmentPickerThreeState(); +} + +class _DepartmentPickerThreeState extends State { + String selectedId = ''; + String selectedPDId = ''; + String selectedName = ''; + Set expandedSet = {}; + + List original = []; + List filtered = []; + bool loading = true; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + // 初始均为空 + selectedId = ''; + selectedName = ''; + selectedPDId = ''; + expandedSet = {}; + _searchController.addListener(_onSearchChanged); + + original = (widget.listdata as List) + .whereType>() + .map((json) => CategoryTypeThree.fromJson(json)) // 显式指定map的泛型类型 + .toList(); + filtered = original; + loading = false; + + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase().trim(); + setState(() { + filtered = query.isEmpty ? original : _filterCategories(original, query); + }); + } + + List _filterCategories(List list, String query) { + List result = []; + for (var cat in list) { + final children = _filterCategories(cat.children, query); + if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) { + result.add(CategoryTypeThree(id: cat.id, name: cat.name,pdId:cat.pdId, children: children)); + } + } + return result; + } + + Widget _buildRow(CategoryTypeThree cat, int indent) { + final hasChildren = cat.children.isNotEmpty; + final isExpanded = expandedSet.contains(cat.id); + final isSelected = cat.id == selectedId; + return Column( + children: [ + InkWell( + onTap: () { + setState(() { + if (hasChildren) { + isExpanded ? expandedSet.remove(cat.id) : expandedSet.add(cat.id); + selectedPDId=cat.pdId; + }else{ + selectedPDId=cat.id; + } + selectedId = cat.id; + selectedName = cat.name; + + }); + }, + child: Container( + color: Colors.white, + child: Row( + children: [ + SizedBox(width: 16.0 * indent), + SizedBox( + width: 24, + child: hasChildren + ? Icon(isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded, + size: 35, color: Colors.grey[600]) + : const SizedBox.shrink(), + ), + const SizedBox(width: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text(cat.name), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: Colors.blue, + ), + ), + ], + ), + ), + ), + if (hasChildren && isExpanded) + ...cat.children.map((c) => _buildRow(c, indent + 1)), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.7, + color: Colors.white, + child: Column( + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(fontSize: 16)), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SearchBarWidget( + controller: _searchController, + isShowSearchButton: false, + onSearch: (keyboard) { + + }, + ), + ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + widget.onSelected(selectedId, selectedName,selectedPDId); + }, + child: const Text('确定', style: TextStyle(fontSize: 16, color: Colors.blue)), + ), + ], + ), + ), + Divider(), + Expanded( + child: loading + ? const Center(child: CircularProgressIndicator()) + : Container( + color: Colors.white, + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/customWidget/department_picker_two.dart b/lib/customWidget/department_picker_two.dart new file mode 100644 index 0000000..6e69073 --- /dev/null +++ b/lib/customWidget/department_picker_two.dart @@ -0,0 +1,249 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import '../tools/tools.dart'; // 包含 SessionService + +// 数据模型 +class CategoryType { + final String id; + final String name; + final String pdId; + final List children; + + CategoryType({ + required this.id, + required this.name, + required this.pdId, + this.children = const [], + }); + + factory CategoryType.fromJson(Map json) { + return CategoryType( + id: json['id'] != null ? json['id'].toString() : "", + name: json['name'] != null ? json['name'].toString() : "", + pdId: json['parentId'] != null ? json['parentId'].toString() : "", + children: _safeParseChildren(json['childrenList']), + ); + } + + static List _safeParseChildren(dynamic childrenData) { + if (childrenData == null) return []; + if (childrenData is! List) return []; + + final List children = []; + for (var item in childrenData) { + if (item is Map) { + try { + children.add(CategoryType.fromJson(item)); + } catch (e) { + print('解析子项失败: $e'); + } + } + } + return children; + } +} + + + +/// 弹窗回调签名:返回选中项的 id 和 name +typedef DeptSelectCallback = void Function(String id, String name,String pdId); + +class DepartmentPickerTwo extends StatefulWidget { + /// 回调,返回选中部门 id 与 name + final DeptSelectCallback onSelected; + + const DepartmentPickerTwo({Key? key, required this.onSelected}) : super(key: key); + + @override + _DepartmentPickerTwoState createState() => _DepartmentPickerTwoState(); +} + +class _DepartmentPickerTwoState extends State { + String selectedId = ''; + String selectedPDId = ''; + String selectedName = ''; + Set expandedSet = {}; + + List original = []; + List filtered = []; + bool loading = true; + + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + // 初始均为空 + selectedId = ''; + selectedName = ''; + selectedPDId = ''; + expandedSet = {}; + _searchController.addListener(_onSearchChanged); + _loadData(); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + Future _loadData() async { + try { + List raw; + // if (SessionService.instance.departmentJsonStr?.isNotEmpty ?? false) { + // raw = json.decode(SessionService.instance.departmentJsonStr!) as List; + // } else { + // final result = await HiddenDangerApi.getHiddenTreatmentListTree(); + // final String nodes = result['data'] as String; + // SessionService.instance.departmentJsonStr = nodes; + // raw = result['data']; + // } + + final result = await HiddenDangerApi.getHiddenTreatmentListTree(); + raw = result['data']; + + setState(() { + original = raw.map((e) => CategoryType.fromJson(e as Map)).toList(); + filtered = original; + loading = false; + }); + } catch (e) { + setState(() => loading = false); + } + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase().trim(); + setState(() { + filtered = query.isEmpty ? original : _filterCategories(original, query); + }); + } + + List _filterCategories(List list, String query) { + List result = []; + for (var cat in list) { + final children = _filterCategories(cat.children, query); + if (cat.name.toLowerCase().contains(query) || children.isNotEmpty) { + result.add(CategoryType(id: cat.id, name: cat.name,pdId:cat.pdId, children: children)); + } + } + return result; + } + + Widget _buildRow(CategoryType cat, int indent) { + final hasChildren = cat.children.isNotEmpty; + final isExpanded = expandedSet.contains(cat.id); + final isSelected = cat.id == selectedId; + return Column( + children: [ + InkWell( + onTap: () { + setState(() { + if (hasChildren) { + isExpanded ? expandedSet.remove(cat.id) : expandedSet.add(cat.id); + selectedPDId=cat.pdId; + }else{ + selectedPDId=cat.id; + } + selectedId = cat.id; + selectedName = cat.name; + + }); + }, + child: Container( + color: Colors.white, + child: Row( + children: [ + SizedBox(width: 16.0 * indent), + SizedBox( + width: 24, + child: hasChildren + ? Icon(isExpanded ? Icons.arrow_drop_down_rounded : Icons.arrow_right_rounded, + size: 35, color: Colors.grey[600]) + : const SizedBox.shrink(), + ), + const SizedBox(width: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text(cat.name), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + color: Colors.blue, + ), + ), + ], + ), + ), + ), + if (hasChildren && isExpanded) + ...cat.children.map((c) => _buildRow(c, indent + 1)), + ], + ); + } + + @override + Widget build(BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.7, + color: Colors.white, + child: Column( + children: [ + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(fontSize: 16)), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SearchBarWidget( + controller: _searchController, + isShowSearchButton: false, + onSearch: (keyboard) { + + }, + ), + ), + ), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + widget.onSelected(selectedId, selectedName,selectedPDId); + }, + child: const Text('确定', style: TextStyle(fontSize: 16, color: Colors.blue)), + ), + ], + ), + ), + Divider(), + Expanded( + child: loading + ? const Center(child: CircularProgressIndicator()) + : Container( + color: Colors.white, + child: ListView.builder( + itemCount: filtered.length, + itemBuilder: (ctx, idx) => _buildRow(filtered[idx], 0), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/customWidget/dotted_border_box.dart b/lib/customWidget/dotted_border_box.dart new file mode 100644 index 0000000..58375ff --- /dev/null +++ b/lib/customWidget/dotted_border_box.dart @@ -0,0 +1,45 @@ + +import 'package:flutter/material.dart'; +import 'package:dotted_border/dotted_border.dart'; + +class DottedBorderBox extends StatelessWidget { + final Widget? child; + final Color color; + final double strokeWidth; + final List dashPattern; + final BorderRadius borderRadius; + final EdgeInsets padding; + final StrokeCap strokeCap; + + const DottedBorderBox({ + super.key, + this.child, + this.color = Colors.black26, + this.strokeWidth = 1.5, + this.dashPattern = const [6, 3], + this.borderRadius = const BorderRadius.all(Radius.circular(8)), + this.padding = const EdgeInsets.all(8), + this.strokeCap = StrokeCap.butt, + }); + + @override + Widget build(BuildContext context) { + return DottedBorder( + options: RoundedRectDottedBorderOptions( + // 控制内边距(虚线与外部的间隔) + borderPadding: EdgeInsets.zero, + // 控制虚线与 child 的间距(虚线内侧留白) + padding: padding, + color: color, + strokeWidth: strokeWidth, + dashPattern: dashPattern, + strokeCap: strokeCap, radius: Radius.circular(0), + // 如果需要,可以传 gradient 等 + ), + child: ClipRRect( + borderRadius: borderRadius, + child: child ?? const SizedBox.shrink(), + ), + ); + } +} diff --git a/lib/customWidget/full_screen_video_page.dart b/lib/customWidget/full_screen_video_page.dart new file mode 100644 index 0000000..9862a03 --- /dev/null +++ b/lib/customWidget/full_screen_video_page.dart @@ -0,0 +1,167 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; +import 'package:chewie/chewie.dart'; + +/// 弹窗组件:支持 本地文件 / 网络视频 自动识别 +class VideoPlayerPopup extends StatefulWidget { + final String videoUrl; + const VideoPlayerPopup({Key? key, required this.videoUrl}) : super(key: key); + + @override + State createState() => _VideoPlayerPopupState(); +} + +class _VideoPlayerPopupState extends State { + late VideoPlayerController _videoController; + ChewieController? _chewieController; + bool _isNetwork = false; + bool _initializing = true; + String? _error; + + @override + void initState() { + super.initState(); + _initController(); + } + + Future _initController() async { + try { + final uri = Uri.tryParse(widget.videoUrl); + final scheme = uri?.scheme?.toLowerCase() ?? ''; + + // 判定是否网络视频(包括 http/https/rtsp/rtmp) + _isNetwork = (scheme == 'http' || scheme == 'https' || scheme == 'rtsp' || scheme == 'rtmp'); + + if (_isNetwork) { + // 网络视频 + _videoController = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)); + } else { + // 本地视频:支持 file:// 开头或直接是本地路径 + if (scheme == 'file' && uri != null) { + _videoController = VideoPlayerController.file(File(uri.toFilePath())); + } else { + // 直接当做本地路径处理(例如 /storage/... 或 沙盒内路径) + _videoController = VideoPlayerController.file(File(widget.videoUrl)); + } + } + + // 初始化 VideoPlayerController + await _videoController.initialize(); + + // 在视频初始化完成后创建 ChewieController(以确保 aspectRatio 可用) + _chewieController?.dispose(); + _chewieController = ChewieController( + videoPlayerController: _videoController, + autoPlay: true, + looping: false, + showOptions: false, + allowFullScreen: true, + allowPlaybackSpeedChanging: true, + allowMuting: true, + showControlsOnInitialize: true, + // 不要在这里强制颜色(你可以自定义),但保留示例: + materialProgressColors: ChewieProgressColors( + playedColor: Colors.blue, + backgroundColor: Colors.white, + handleColor: Colors.blue, + bufferedColor: Colors.white, + ), + aspectRatio: _videoController.value.aspectRatio > 0 + ? _videoController.value.aspectRatio + : 16 / 9, + errorBuilder: (context, errorMessage) { + return Center( + child: Text( + errorMessage ?? '视频播放错误', + style: const TextStyle(color: Colors.white), + ), + ); + }, + ); + + if (!mounted) return; + setState(() { + _initializing = false; + }); + } catch (e, st) { + // 捕获异常并展示错误信息 + debugPrint('Video init error: $e\n$st'); + if (!mounted) return; + setState(() { + _initializing = false; + _error = e.toString(); + }); + } + } + + @override + void dispose() { + _chewieController?.dispose(); + _videoController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Material( + color: Colors.transparent, + child: Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.9, + maxHeight: 500, + ), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + // 视频播放器 或 错误 / 加载指示 + Positioned.fill( + child: _buildPlayerBody(), + ), + + // 关闭按钮 + Positioned( + top: 4, + right: 4, + child: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPlayerBody() { + if (_initializing) { + return const Center(child: CircularProgressIndicator()); + } + + if (_error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + '播放失败:$_error', + style: const TextStyle(color: Colors.white), + ), + ), + ); + } + + if (_chewieController != null && _videoController.value.isInitialized) { + return Chewie(controller: _chewieController!); + } + + return const Center(child: Text('无法播放视频', style: TextStyle(color: Colors.white))); + } +} diff --git a/lib/customWidget/item_list_widget.dart b/lib/customWidget/item_list_widget.dart new file mode 100644 index 0000000..d9a725a --- /dev/null +++ b/lib/customWidget/item_list_widget.dart @@ -0,0 +1,1520 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/single_image_viewer.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:flutter/services.dart'; + +class ItemListWidget { + static const Color detailtextColor = Colors.black54; + static const double requiredInset = 0; + static const double horizontal_inset = 12; + static const double vertical_inset = 5; + + /// 单行水平排列: + /// - 可编辑时:标题 + TextField + /// - 不可编辑时:标题 + 带省略号的文本 + + static Widget singleLineTitleText({ + required String label, // 标题文本 + required bool isEditable, // 是否可编辑 + String? text, // 显示的初始文本(编辑/非编辑模式都会显示) + String hintText = '请输入', + double fontSize = 14, // 字体大小 + bool isRequired = true, + bool strongRequired = false, + ValueChanged? onChanged, + ValueChanged? onFieldSubmitted, + int maxLines = 5, + bool showMaxLength = false, + + // 新增:数字输入控制 + bool isNumericInput = false, + int maxDecimalPlaces = 2, + TextInputType keyboardType = TextInputType.text, + }) { + // 数字输入键盘 + final actualKeyboardType = + isNumericInput + ? const TextInputType.numberWithOptions(decimal: true) + : keyboardType; + + // 数字输入格式化器 + final List? numericFormatters = + isNumericInput + ? [ + FilteringTextInputFormatter.allow(RegExp(r'[\d\.]')), + TextInputFormatter.withFunction((oldValue, newValue) { + final newText = newValue.text; + + if (newText.isEmpty) return newValue; + + if (newText.split('.').length > 2) return oldValue; + + final regex = RegExp( + r'^\d*\.?\d{0,' + maxDecimalPlaces.toString() + r'}$', + ); + + if (regex.hasMatch(newText)) return newValue; + + return oldValue; + }), + ] + : null; + + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Row( + mainAxisAlignment: + isEditable + ? MainAxisAlignment.start + : MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if ((isRequired && isEditable) || strongRequired) + Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(width: 8), + + /// 可编辑模式 + isEditable + ? Expanded( + child: TextFormField( + initialValue: text ?? '', + autofocus: false, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + keyboardType: actualKeyboardType, + maxLength: showMaxLength ? 120 : null, + style: TextStyle(fontSize: fontSize), + maxLines: 1, + inputFormatters: numericFormatters, + decoration: InputDecoration( + isDense: true, + hintText: hintText, + contentPadding: EdgeInsets.symmetric(vertical: 8), + ), + ), + ) + /// 只读模式 + : Expanded( + child: Text( + text ?? '', + maxLines: maxLines, + style: TextStyle(fontSize: fontSize, color: detailtextColor), + textAlign: TextAlign.right, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + /// 多行垂直排列: + /// - 可编辑时:标题 + 可扩展的多行 TextField + /// - 不可编辑时:标题 + 带滚动的多行文本 + static Widget multiLineTitleTextField({ + required String label, // 标题文本 + required bool isEditable, // 是否可编辑 + TextEditingController? controller, // 编辑时使用的控制器 + String? text, // 不可编辑时显示的文本 + double fontSize = 14, // 字体大小 + double height = 110, // 整体高度 + bool isRequired = true, + String hintText = '请输入', + ValueChanged? onChanged, + bool showMaxLength = false, + }) { + return Container( + // 统一左右 padding,保证标题和内容在同一左侧基线 + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 12), + height: height, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isRequired && isEditable) + Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Expanded( + child: + isEditable + ? TextFormField( + autofocus: false, + initialValue: controller == null ? text : null, + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + onChanged: onChanged, + maxLength: showMaxLength ? 120 : null, + // 垂直顶部对齐 + textAlignVertical: TextAlignVertical.top, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: hintText, + // 去掉 TextField 默认内边距 + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ) + : SingleChildScrollView( + // 去掉多余的 padding + padding: EdgeInsets.zero, + child: Text( + text ?? '', + style: TextStyle( + fontSize: fontSize, + color: detailtextColor, + ), + ), + ), + ), + ], + ), + ); + } + + static Widget multiLineAutoTitleTextField({ + required String label, + required bool isEditable, + TextEditingController? controller, + String? text, + double fontSize = 14, + double height = 110, // 编辑时保留原行为 + bool isRequired = true, + String hintText = '请输入', + ValueChanged? onChanged, + bool showMaxLength = false, + double? maxDisplayHeight, // 可选:在父无高度约束时作为“可用高度”参考 + }) { + return LayoutBuilder( + builder: (context, constraints) { + // 内边距(与你的 UI 保持一致) + const horizontalPadding = 12.0; + const verticalPadding = 5.0; + final content = text ?? ''; + final textStyle = TextStyle(fontSize: fontSize, color: detailtextColor); + + // 编辑状态:保持原来固定 height 与 expands:true 的行为 + if (isEditable) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + height: height, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isRequired) + const Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Expanded( + child: TextFormField( + autofocus: false, + initialValue: controller == null ? text : null, + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + onChanged: onChanged, + maxLength: showMaxLength ? 120 : null, + textAlignVertical: TextAlignVertical.top, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: hintText, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ), + ), + ], + ), + ); + } + + // ---------- 不可编辑:测量并决定是否需要滚动 ---------- + // 计算可用于内容的最大宽度(减去左右 padding) + final maxWidth = + (constraints.maxWidth.isFinite + ? constraints.maxWidth + : MediaQuery.of(context).size.width) - + horizontalPadding * 2; + final safeMaxWidth = + maxWidth > 0 + ? maxWidth + : MediaQuery.of(context).size.width - horizontalPadding * 2; + + // 使用 TextPainter 测量文本高度(不限制行数) + final tp = TextPainter( + text: TextSpan(text: content, style: textStyle), + textDirection: TextDirection.ltr, + textWidthBasis: TextWidthBasis.parent, + ); + tp.layout(maxWidth: safeMaxWidth); + final textHeight = tp.size.height; + + // 估算标签与间距所需高度 + final labelHeight = fontSize + 8; // label + 上下间距估算 + final neededHeight = labelHeight + 8 + textHeight + verticalPadding * 2; + + // availableHeight:若父是 bounded 则用 constraints.maxHeight,否则使用 maxDisplayHeight 或屏幕高度比例作为阈值 + double availableHeight; + if (constraints.maxHeight.isFinite) { + availableHeight = constraints.maxHeight; + } else { + availableHeight = + maxDisplayHeight ?? MediaQuery.of(context).size.height * 0.4; + } + + final needsScroll = neededHeight > availableHeight; + + // 返回 Widget(不固定高度) + return Container( + padding: const EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: horizontalPadding, + ), + // 不设置 height,让其在可扩展父容器中自然撑开 + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (isRequired) + const Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + // 如果需要滚动则约束高度并内包 SingleChildScrollView + if (needsScroll) + // ConstrainedBox 限制最大高度,避免无限膨胀 + ConstrainedBox( + constraints: BoxConstraints( + // 留出 label 的高度,最大不超过 availableHeight - labelHeight + maxHeight: (availableHeight - labelHeight - 16).clamp( + 80.0, + availableHeight, + ), + ), + child: SingleChildScrollView( + padding: EdgeInsets.zero, + physics: const BouncingScrollPhysics(), + child: Text(content, style: textStyle), + ), + ) + else + // 父高度足够,直接展示文本(自然撑高) + Text(content, style: textStyle), + ], + ), + ); + }, + ); + } + + /// 单行可点击选择: + /// - 可编辑时:标题 + “请选择”提示 + 右箭头 + /// - 不可编辑时:标题 + 文本内容 + static Widget selectableLineTitleTextRightButton({ + required String label, // 标题文本 + required bool isEditable, // 是否可点击 + required String text, // 显示内容或提示 + VoidCallback? onTap, // 点击回调 + double fontSize = 14, // 字体大小 + bool isClean = false, + bool isTip = false, + VoidCallback? onTapClean, // 清除回调 + VoidCallback? onTapTip, // 提醒回调 + bool isRequired = true, + String cleanText = '清除', + bool strongRequired = false, + + double horizontalnum = horizontal_inset, + double verticalInset = vertical_inset, + + }) { + return InkWell( + onTap: isEditable ? onTap : null, + child: Container( + padding: EdgeInsets.symmetric( + vertical: verticalInset, + horizontal: horizontalnum, + ), + child: Row( + children: [ + // 1. 标题 + Row( + children: [ + if ((isRequired && isEditable) || strongRequired) + Text('* ', style: TextStyle(color: Colors.red)), + Row( + children: [ + Text( + label, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + if (isTip) + Column( + children: [ + IconButton( + onPressed: onTapTip, + icon: Icon( + Icons.error_outline, + color: Colors.blue, + size: 20, + ), + ), + const SizedBox(), + ], + ), + ], + ), + ], + ), + if (isClean) + Column( + children: [ + CustomButton( + text: cleanText, + height: 20, + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 0), + textStyle: TextStyle(fontSize: 11, color: Colors.white), + borderRadius: 10, + backgroundColor: + cleanText.contains('清除') ? Colors.red : Colors.green, + onPressed: onTapClean, + ), + SizedBox(height: 20), + ], + ), + const SizedBox(width: 8), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: Text( + text.isNotEmpty ? text : (isEditable ? '请选择' : ''), + maxLines: 5, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: fontSize, + color: + isEditable + ? (text == '请选择' + ? Colors.black87 + : Colors.black) + : detailtextColor, + ), + ), + ), + if (isEditable) + const Padding( + padding: EdgeInsets.only(left: 4), + child: Icon(Icons.chevron_right, size: 20), + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// 单行可点击选择: + /// - 可编辑时:标题 + “请选择”提示 + 文本框 + /// - 不可编辑时:标题 + 文本内容 + static Widget selectableLineTitleTextField({ + required String label, + required bool isEditable, + required String text, + VoidCallback? onTap, + double fontSize = 14, + bool isClean = false, + VoidCallback? onTapClean, + bool isRequired = true, + String cleanText = '清除', + TextEditingController? controller, + }) { + return InkWell( + onTap: isEditable ? onTap : null, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 标题部分 + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isRequired && isEditable) + const Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + // const SizedBox(width: 5), + // 右侧 TextField + 清除按钮 + Expanded( + child: Row( + children: [ + // 清除按钮 + if (isClean && onTapClean != null) + Column( + children: [ + CustomButton( + text: cleanText, + height: 20, + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 0, + ), + textStyle: TextStyle( + fontSize: 11, + color: Colors.white, + ), + borderRadius: 10, + backgroundColor: + cleanText == '清除' ? Colors.red : Colors.green, + onPressed: onTapClean, + ), + SizedBox(height: 20), + ], + ), + // 输入框 + Expanded( + child: TextField( + controller: controller, + autofocus: false, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: '请输入', + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric(vertical: 8), + border: InputBorder.none, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// 两行垂直布局: + /// 第一行:可点击选择(带箭头)或仅显示标题 + /// 第二行:多行输入框或多行文本展示 + static Widget twoRowSelectableTitleText({ + required String label, // 第一行标题 + required bool isEditable, // 是否可编辑 + required String text, // 显示内容或提示 + TextEditingController? controller, // 第二行编辑控制器 + VoidCallback? onTap, // 第一行点击回调 + double fontSize = 14, // 字体大小 + double row2Height = 80, // 第二行高度 + bool isRequired = true, + bool showSelect = true, //是否显示选择 + }) { + return Container( + padding: EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:可点击区域或纯文本标题 + InkWell( + onTap: isEditable ? onTap : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (isRequired && isEditable) + Text('* ', style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + if (showSelect) + Row( + children: [ + Text( + isEditable ? '请选择' : '', + style: TextStyle( + fontSize: fontSize, + color: isEditable ? Colors.black : detailtextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (isEditable) const Icon(Icons.chevron_right), + ], + ), + ], + ), + ), + const SizedBox(height: 8), + Container( + height: row2Height, + padding: const EdgeInsets.symmetric(vertical: 8), + child: + isEditable + ? TextField( + autofocus: false, + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: '请输入', + //contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ) + : SingleChildScrollView( + padding: EdgeInsets.zero, + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + color: detailtextColor, + ), + ), + ), + ), + ], + ), + ); + } + + /// 两行垂直布局: + /// 标题 + 按钮 + /// 第二行:多行输入框或多行文本展示 + static Widget twoRowButtonTitleText({ + required String label, // 第一行标题 + required bool isEditable, // 是否可编辑 + bool isInput = true, // 是否可输入 + required String text, // 显示内容或提示 + TextEditingController? controller, // 第二行编辑控制器 + required VoidCallback? onTap, // 第一行点击回调 + String buttonText = '选择其他', + required String hintText, + double fontSize = 14, // 字体大小 + double row2Height = 80, // 第二行高度 + bool isRequired = true, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:标题 + 按钮 + InkWell( + child: Row( + children: [ + Flexible( + fit: FlexFit.loose, // loose 模式下它可以比最大宽度更小 + child: Row( + children: [ + if (isRequired && isEditable) + Text('* ', style: TextStyle(color: Colors.red)), + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + if (isEditable) + CustomButton( + text: buttonText, + height: 30, + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 10, + ), + textStyle: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + backgroundColor: Colors.blue, + onPressed: onTap, + ), + ], + ), + ), + const SizedBox(height: 8), + + Container( + height: row2Height, + padding: const EdgeInsets.symmetric(vertical: 8), + child: + (isEditable && isInput) + ? TextField( + autofocus: false, + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: hintText, + //contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ) + : SingleChildScrollView( + padding: EdgeInsets.zero, + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + color: detailtextColor, + ), + ), + ), + ), + ], + ), + ); + } + + /// 单行布局: + /// 标题 + 文字 + 按钮 + static Widget OneRowButtonTitleText({ + required String label, // 标题 + required String text, // 显示内容或提示 + required VoidCallback? onTap, // 第一行点击回调 + double fontSize = 14, // 字体大小 + bool isEdit = true, + String buttonText = '气体分析详情', + double horizontalnum = horizontal_inset, + }) { + return Container( + color: Colors.white, + padding: EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontalnum, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 15), + Expanded( + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: fontSize, + color: detailtextColor, + ), + ), + ), + ], + ), + ), + if (isEdit) + CustomButton( + text: buttonText, + height: 30, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + textStyle: TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + backgroundColor: Colors.blue, + onPressed: onTap, + ), + ], + ), + ); + } + + /// 单行布局: + /// 标题 + 按钮 + static Widget OneRowButtonTitle({ + required String label, // 标题 + required String buttonText, // 按钮文字 + required VoidCallback onTap, // 第一行点击回调 + double fontSize = 14, // 字体大小 + Color btnColor = Colors.blue, + bool isRequired = false, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text("* ", style: TextStyle(color: Colors.red)), + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + CustomButton( + text: buttonText, + height: 30, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), + backgroundColor: btnColor, + onPressed: onTap, + ), + ], + ), + ); + } + + /// 单行布局: + /// 标题 + 按钮(挨着) + static Widget OneRowStartButtonTitle({ + required String label, // 标题 + String buttonText = '气体分析详情', // 按钮文字 + String text = '', // 标题 + required VoidCallback onTap, // 点击回调 + double fontSize = 14, // 字体大小 + Color btnColor = Colors.blue, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold), + ), + CustomButton( + text: buttonText, + height: 30, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), + backgroundColor: btnColor, + onPressed: onTap, + ), + ], + ), + ); + } + + /// 单行布局: + /// 标题 + 网络图片 + static Widget OneRowImageTitle({ + required String label, // 标题 + required String imgPath, // 图片路径 + double fontSize = 14, // 字体大小 + Color btnColor = Colors.blue, + bool isRequired = false, + String text = '', + void Function(String)? onTapCallBack, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Column( + children: [ + GestureDetector( + onTap: () { + if (onTapCallBack != null) + onTapCallBack('${ApiService.baseImgPath}$imgPath'); + }, + child: + imgPath.isNotEmpty + ? Image.network( + '${ApiService.baseImgPath}${imgPath}', + width: 80, + height: 80, + ) + : SizedBox(), + ), + if (text.isNotEmpty) Text(text), + ], + ), + ], + ), + ); + } + + /// 单行布局: + /// 图片 + 标题 +箭头 + static Widget OneRowImageArrowTitle({ + required String label, // 标题 + required String imgPath, // 图片路径 + double fontSize = 14, // 字体大小 + Color btnColor = Colors.black, + }) { + return Card( + shape: RoundedRectangleBorder( + // 形状 + borderRadius: BorderRadius.circular(8), // 圆角 + ), + color: Colors.white, + child: Padding( + padding: EdgeInsets.all(14), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset(width: 40, height: 40, imgPath), + SizedBox(width: 20), + Text(label, style: TextStyle(fontSize: 14)), + Spacer(), + Icon(Icons.chevron_right, color: Colors.grey[400]), + ], + ), + ), + ); + } + + /// 两行垂直布局: + /// 标题 + /// 第二行:图片 + static Widget twoRowTitleAndImages({ + required String title, // 第一行标题 + required List? imageUrls, + double row2Height = 80, // 第二行高度 + double fontSize = 14, // 字体大小 + void Function(String)? onTapCallBack, + bool isRequired = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + // 标题部分 + if (title.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Text( + title, + style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold), + ), + ), + + // 图片横向滚动区域 + SizedBox( + height: 80, // 图片区域固定高度 + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + itemCount: imageUrls?.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(right: 8), // 图片间距 + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GestureDetector( + onTap: () { + if (onTapCallBack != null) + onTapCallBack( + '${ApiService.baseImgPath}${imageUrls![index] ?? ''}', + ); + }, + child: Image.network( + '${ApiService.baseImgPath}${imageUrls![index] ?? ''}', + width: 80, + // 图片宽度 + height: 80, + // 图片高度 + fit: BoxFit.fill, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 80, + height: 80, + color: Colors.grey[200], + child: const Center( + child: CircularProgressIndicator(), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 80, + color: Colors.transparent, + child: SizedBox(), + ); + }, + ), + ), + ), + ); + }, + ), + ), + ], + ); + } + + /// 多行垂直布局: + /// 标题+按钮 + /// 编辑框列表,多个编辑框可删除 + static Widget mulRowTitleAndTextField({ + required String label, // 第一行标题 + required bool isEditable, // 是否可编辑 + required String text, // 显示内容或提示 + TextEditingController? controller, // 第二行编辑控制器 + required VoidCallback? onTap, // 第一行点击回调 + required String hintText, + double fontSize = 14, // 字体大小 + double row2Height = 80, // 第二行高度 + bool isRequired = true, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:标题 + 按钮 + InkWell( + child: Row( + children: [ + Flexible( + fit: FlexFit.loose, // loose 模式下它可以比最大宽度更小 + child: Row( + children: [ + if (isRequired && isEditable) + Text('* ', style: TextStyle(color: Colors.red)), + Flexible( + child: Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(width: 8), + if (isEditable) + CustomButton( + text: "选择其他", + height: 30, + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 5, + ), + backgroundColor: Colors.blue, + onPressed: onTap, + ), + ], + ), + ), + const SizedBox(height: 8), + + Container( + height: row2Height, + padding: const EdgeInsets.symmetric(vertical: 8), + child: + isEditable + ? TextField( + autofocus: false, + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: null, + expands: true, + style: TextStyle(fontSize: fontSize), + decoration: InputDecoration( + hintText: hintText, + //contentPadding: EdgeInsets.zero, + border: InputBorder.none, + ), + ) + : SingleChildScrollView( + padding: EdgeInsets.zero, + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + color: detailtextColor, + ), + ), + ), + ), + ], + ), + ); + } + + /// 多行垂直布局: + /// 标题、图片、说明、签字信息 + static Widget mulColumnRowTitleAndImages({ + required String title, // 第一行标题 + required List? imageUrls, + required String text, // 描述 + required List? signUrls, + + /// 签字 + required List? signTimes, + + /// 签字时间 + double row2Height = 80, // 第二行高度 + double fontSize = 14, // 字体大小 + void Function(String)? onTapCallBack, + bool isRequired = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + // 标题部分 + Padding( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Text( + title, + style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold), + ), + ), + + // 图片横向滚动区域 + SizedBox( + height: 80, // 图片区域固定高度 + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + itemCount: imageUrls?.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(right: 8), // 图片间距 + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GestureDetector( + onTap: () { + if (onTapCallBack != null) + onTapCallBack( + '${ApiService.baseImgPath}${imageUrls![index] ?? ''}', + ); + }, + child: Image.network( + '${ApiService.baseImgPath}${imageUrls![index] ?? ''}', + width: 80, + // 图片宽度 + height: 80, + // 图片高度 + fit: BoxFit.fill, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + width: 80, + height: 80, + color: Colors.grey[200], + child: const Center( + child: CircularProgressIndicator(), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + width: 80, + height: 80, + color: Colors.transparent, + child: SizedBox(), + ); + }, + ), + ), + ), + ); + }, + ), + ), + Row(), + ], + ); + } + + /// 两行垂直布局: + /// 标题 + /// 第二行:多行文本展示 + static Widget twoRowTitleText({ + required String label, // 第一行标题 + required String text, // 显示内容或提示 + double fontSize = 14, // 字体大小 + bool isRequired = true, + }) { + return Container( + padding: const EdgeInsets.symmetric( + vertical: vertical_inset, + horizontal: horizontal_inset, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:标题 + Text( + label, + style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Text( + text, + maxLines: 5, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: fontSize, color: detailtextColor), + ), + ], + ), + ); + } + + static Widget itemContainer( + Widget child, { + double horizontal = horizontal_inset, + double vertical = vertical_inset, + }) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric(horizontal: horizontal, vertical: vertical), + child: child, + ); + } + + // static Widget aaa({}){ + // return + // } + /// 安全环保检查步骤 + static Widget buildFlowStepItem({ + required List> flowList, + }) { + final int lastDoneIndex = flowList.lastIndexWhere((e) => e['STATUS'] == 1); + + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: flowList.length + 1, // +1 用来放标题 + itemBuilder: (context, i) { + if (i == 0) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + '查看流程图', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ); + } + final idx = i - 1; + final item = flowList[idx]; + final bool isFirst = idx == 0; + final bool isLast = idx == flowList.length - 1; + + // 根据 lastDoneIndex 自动计算“进行中” + final int status; + if (idx <= lastDoneIndex) { + status = 1; // 已完成 + } else if (idx == lastDoneIndex + 1) { + status = 0; // 进行中 + } else { + status = -1; // 未到达 + } + // 依据状态设色 + final Color dotColor = + status == 1 + ? Colors.green + : (status == 0 ? Colors.blue : Colors.grey); + final Color textColor = + status == 1 + ? Colors.green + : (status == 0 ? Colors.blue : Colors.black); + + return ListTile( + visualDensity: VisualDensity(vertical: -4), + + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + leading: Container( + width: 24, + alignment: Alignment.center, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + // 上方线段或占位 + isFirst + ? SizedBox(height: 6 + 5) + : Expanded( + child: Container(width: 1, color: Colors.grey[300]), + ), + // 圆点 + CircleAvatar(radius: 6, backgroundColor: dotColor), + // 下方线段或占位 + isLast + ? SizedBox(height: 6 + 5) + : Expanded( + child: Container(width: 1, color: Colors.grey[300]), + ), + ], + ), + ), + title: Text( + item['STEP_NAME'] ?? '', + style: TextStyle(color: textColor, fontSize: 15), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (item['SIGN_USER'] != null) ...[ + Text( + item['SIGN_USER'], + style: TextStyle(color: textColor, fontSize: 13), + ), + ] else if (item['FINISHED_SIGN_USER'] != null) ...[ + Text( + item['FINISHED_SIGN_USER'], + style: TextStyle(color: textColor, fontSize: 13), + ), + ] else if (item['ACT_USER_NAME'] != null) ...[ + Text( + item['ACT_USER_NAME'], + style: TextStyle(color: textColor, fontSize: 13), + ), + ], + if (item['ACT_TIME'] != null) + Text( + item['ACT_TIME'], + style: TextStyle(color: textColor, fontSize: 13), + ), + ], + ), + ); + }, + ); + } + + /// 特殊作业步骤流程图 + static Widget specialBuildFlowStepItem({ + required List> flowList, + }) { + // status: 1 已完成, 0 当前步骤, -99 未开始, 2 已打回, -1 已跳过 + final int lastDoneIndex = flowList.lastIndexWhere((e) { + final s = e['status']; + if (s is int) return s == 1; + if (s is String) return int.tryParse(s) == 1; + return false; + }); + + return ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 16), + itemCount: flowList.length + 1, // +1 用来放标题 + itemBuilder: (context, i) { + if (i == 0) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + '查看流程图', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ); + } + + final idx = i - 1; + final item = flowList[idx]; + final bool isFirst = idx == 0; + final bool isLast = idx == flowList.length - 1; + + // 尝试读取 item 中的 status(支持 int 或可解析为 int 的 String) + int? statusFromItem; + final rawStatus = item['status']; + if (rawStatus is int) { + statusFromItem = rawStatus; + } else if (rawStatus is String) { + statusFromItem = int.tryParse(rawStatus); + } + + // 如果 item 中没有 status,则回退到根据 lastDoneIndex 推断的逻辑 + final int status = + statusFromItem ?? + (idx <= lastDoneIndex + ? 1 // 已完成 + : (idx == lastDoneIndex + 1 ? 0 : -99)); // 0=当前,-99=未开始 + + // 颜色映射:1 -> 绿,0 -> 蓝,其它 -> 灰 + final Color dotColor = + status == 1 + ? Colors.green + : (status == 0 ? Colors.blue : Colors.grey); + final Color textColor = + status == 1 + ? Colors.green + : (status == 0 ? Colors.blue : Colors.black); + + // 使用新的字段名:stepName, actUserName, actTime + final String title = (item['stepName'] ?? '').toString(); + final String? user = + (item['actUserName'] ?? item['ACT_USER_NAME'] ?? item['SIGN_USER']) + ?.toString(); + final String? time = (item['actTime'] ?? item['ACT_TIME'])?.toString(); + + return ListTile( + visualDensity: VisualDensity(vertical: -4), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + leading: SizedBox( + width: 24, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + // 上方线段或占位 + if (isFirst) + const SizedBox(height: 11) // 保持与原来相似的间距 + else + Expanded(child: Container(width: 1, color: Colors.grey[300])), + // 圆点 + CircleAvatar(radius: 6, backgroundColor: dotColor), + // 下方线段或占位 + if (isLast) + const SizedBox(height: 11) + else + Expanded(child: Container(width: 1, color: Colors.grey[300])), + ], + ), + ), + title: Text(title, style: TextStyle(color: textColor, fontSize: 15)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (user != null && user.isNotEmpty) + Text(user, style: TextStyle(color: textColor, fontSize: 13)), + if (rawStatus == -1) + Text('已跳过', style: TextStyle(color: textColor, fontSize: 13)), + if (time != null && time.isNotEmpty) + Text(time, style: TextStyle(color: textColor, fontSize: 13)), + ], + ), + ); + }, + ); + } +} diff --git a/lib/customWidget/photo_picker_row.dart b/lib/customWidget/photo_picker_row.dart new file mode 100644 index 0000000..26dedcb --- /dev/null +++ b/lib/customWidget/photo_picker_row.dart @@ -0,0 +1,992 @@ +// .dart +import 'dart:async'; +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/full_screen_video_page.dart'; +import 'package:qhd_prevention/customWidget/single_image_viewer.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:video_compress/video_compress.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:path/path.dart' as p; +import 'ItemWidgetFactory.dart'; + +const String kAcceptVideoSectionKey = 'accept_video'; + +/// ---------------------- 全局 MediaBus (轻量事件总线) ---------------------- +class MediaEvent { + final String key; + final MediaEventType type; + final List? paths; + + MediaEvent._(this.key, this.type, [this.paths]); + + factory MediaEvent.clear(String key) => MediaEvent._(key, MediaEventType.clear); + + factory MediaEvent.set(String key, List paths) => + MediaEvent._(key, MediaEventType.set, List.from(paths)); +} + +enum MediaEventType { clear, set } + +class MediaBus { + MediaBus._internal(); + static final MediaBus _instance = MediaBus._internal(); + factory MediaBus() => _instance; + + final StreamController _ctrl = StreamController.broadcast(); + + Stream get stream => _ctrl.stream; + + void emit(MediaEvent ev) { + if (!_ctrl.isClosed) _ctrl.add(ev); + } + + Future dispose() async { + await _ctrl.close(); + } +} +/// ---------------------- /MediaBus ---------------------- + + +/// 媒体选择类型 +enum MediaType { image, video } + +/// ---------- 辅助函数(文件顶部复用) ---------- +bool _isNetworkPath(String? p) { + if (p == null) return false; + final s = p.trim().toLowerCase(); + return s.startsWith('http://') || s.startsWith('https://'); +} + +/// 把路径列表转换为存在的本地 File 列表(过滤掉网络路径与空路径与不存在的本地文件) +/// 注意:这个函数**可能**会同步访问文件系统(existsSync),它只应在用户触发后调用,不应在 build() 中被频繁调用。 +List _localFilesFromPaths(List? paths) { + if (paths == null) return []; + return paths + .map((e) => (e ?? '').toString().trim()) + .where((s) => s.isNotEmpty && !_isNetworkPath(s)) + .where((s) => File(s).existsSync()) + .map((s) => File(s)) + .toList(); +} + +/// 规范化路径列表:trim + 过滤空字符串 +List _normalizePaths(List? src) { + if (src == null) return []; + return src.map((e) => (e ?? '').toString().trim()).where((s) => s.isNotEmpty).toList(); +} + +/// ---------- MediaPickerRow ---------- +// ------------------ 修改后的 MediaPickerRow(完整类) ------------------ +class MediaPickerRow extends StatefulWidget { + final int maxCount; + final MediaType mediaType; + final List? initialMediaPaths; + final ValueChanged> onChanged; + final ValueChanged? onMediaAdded; + final ValueChanged? onMediaRemoved; + final ValueChanged? onMediaRemovedForIndex; + final ValueChanged? onMediaTapped; + final bool isEdit; + final bool isCamera; + /// 默认 false —— 仅在 initState 时读取 initialMediaPaths + final bool followInitialUpdates; + + /// 新增:网格列数(默认 4),可在需要单列/自适应宽度时指定 1 + final int crossAxisCount; + + const MediaPickerRow({ + Key? key, + this.maxCount = 4, + this.mediaType = MediaType.image, + this.initialMediaPaths, + required this.onChanged, + this.onMediaAdded, + this.onMediaRemoved, + this.onMediaRemovedForIndex, + this.onMediaTapped, + this.isEdit = true, + this.isCamera = false, + this.followInitialUpdates = false, // 默认 false + this.crossAxisCount = 4, // 默认 4 列 + }) : super(key: key); + + @override + _MediaPickerGridState createState() => _MediaPickerGridState(); +} + +class _MediaPickerGridState extends State { + final ImagePicker _picker = ImagePicker(); + late List _mediaPaths; + bool _isProcessing = false; + + /// 缓存每个本地路径是否存在(避免在 build 中反复同步 IO) + final Map _localExistsCache = {}; + + @override + void initState() { + super.initState(); + // 初始化内部路径(保留网络路径与本地路径) + _mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList(); + + // 预先检查一次本地文件是否存在(只在 init 时做一次同步检查) + for (final pth in _mediaPaths) { + final t = pth.trim(); + if (!_isNetworkPath(t)) { + try { + _localExistsCache[t] = File(t).existsSync(); + } catch (_) { + _localExistsCache[t] = false; + } + } else { + _localExistsCache[pth] = false; + } + } + + // 仅在存在本地真实文件时才把 File 列表回调给外部(避免父组件用这个回调覆盖只有网络路径的数据) + final initialLocalFiles = _localFilesFromPaths(_mediaPaths); + if (initialLocalFiles.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + widget.onChanged(initialLocalFiles); + }); + } + } + + @override + void didUpdateWidget(covariant MediaPickerRow oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.followInitialUpdates) { + final oldList = _normalizePaths(oldWidget.initialMediaPaths); + final newList = _normalizePaths(widget.initialMediaPaths); + + if (!listEquals(oldList, newList)) { + _mediaPaths = newList.take(widget.maxCount).toList(); + + // 更新本地存在缓存(同步检查,仅在更新时执行) + for (final pth in _mediaPaths) { + final t = pth.trim(); + if (!_localExistsCache.containsKey(t)) { + if (!_isNetworkPath(t)) { + try { + _localExistsCache[t] = File(t).existsSync(); + } catch (_) { + _localExistsCache[t] = false; + } + } else { + _localExistsCache[t] = false; + } + } + } + + if (mounted) setState(() {}); + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + }); + } + } + } + + Future _handlePickedPath(String path) async { + if (!mounted) return; + if (path.isEmpty) return; + + try { + String finalPath = path; + + if (widget.mediaType == MediaType.video) { + final ext = p.extension(path).toLowerCase(); + if (ext != '.mp4') { + setState(() => _isProcessing = true); + try { + final info = await VideoCompress.compressVideo( + path, + quality: VideoQuality.MediumQuality, + deleteOrigin: false, + ); + if (info != null && info.file != null) { + finalPath = info.file!.path; + debugPrint('✅ 转换完成: $path -> $finalPath'); + } else { + throw Exception("转码失败: 返回空文件"); + } + } catch (e) { + debugPrint('❌ 视频转码失败: $e'); + if (mounted) { + ToastUtil.showNormal(context, '视频转码失败'); + } + return; + } finally { + if (mounted) setState(() => _isProcessing = false); + } + } + } + + if (_mediaPaths.length < widget.maxCount) { + setState(() => _mediaPaths.add(finalPath)); + + // 记录缓存(只在添加时检查一次文件是否真实存在) + if (!_isNetworkPath(finalPath)) { + try { + _localExistsCache[finalPath] = File(finalPath).existsSync(); + } catch (_) { + _localExistsCache[finalPath] = false; + } + } else { + _localExistsCache[finalPath] = false; + } + + // 回调仅包含本地真实存在的文件(网络路径不会出现在此回调中) + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(finalPath); + } + } catch (e) { + debugPrint('处理选中媒体失败: $e'); + } + } + + Future _showPickerOptions() async { + if (!widget.isEdit) return; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + builder: (_) => SafeArea( + child: Wrap( + children: [ + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), + title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), + onTap: () { + Navigator.of(context).pop(); + _pickCamera(); + }, + ), + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), + title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), + onTap: () { + Navigator.of(context).pop(); + _pickGallery(); + }, + ), + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: const Icon(Icons.close), + title: const Text('取消'), + onTap: () => Navigator.of(context).pop(), + ), + ], + ), + ), + ); + } + + Future _pickCamera() async { + if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; + + try { + XFile? picked; + if (widget.mediaType == MediaType.image) { + picked = await _picker.pickImage(source: ImageSource.camera); + if (picked != null) { + final path = picked.path; + setState(() => _mediaPaths.add(path)); + + // 记录存在 + try { + _localExistsCache[path] = File(path).existsSync(); + } catch (_) { + _localExistsCache[path] = false; + } + + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(path); + } + } else { + picked = await _picker.pickVideo(source: ImageSource.camera); + if (picked != null) { + await _handlePickedPath(picked.path); + } + } + } catch (e) { + debugPrint('拍摄失败: $e'); + } + } + + Future _pickGallery() async { + if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; + + try { + if (Platform.isIOS) { + final permission = await PhotoManager.requestPermissionExtend(); + debugPrint('iOS photo permission state: $permission'); + + if (permission != PermissionState.authorized && permission != PermissionState.limited) { + if (mounted) { + ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); + } + return; + } + + final remaining = widget.maxCount - _mediaPaths.length; + final List? assets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, + maxAssets: remaining, + gridCount: 4, + ), + ); + + if (assets != null) { + for (final asset in assets) { + if (_mediaPaths.length >= widget.maxCount) break; + try { + final file = await asset.file; + if (file != null) { + final path = file.path; + await _handlePickedPath(path); + } else { + debugPrint('资产获取 file 为空,asset id: ${asset.id}'); + } + } catch (e) { + debugPrint('读取 asset 文件失败: $e'); + } + } + if (mounted) setState(() {}); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } else { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final androidInfo = await deviceInfo.androidInfo; + final int sdkInt = androidInfo.version.sdkInt ?? 0; + + PermissionStatus permissionStatus = PermissionStatus.denied; + + if (sdkInt >= 33) { + if (widget.mediaType == MediaType.image) { + permissionStatus = await Permission.photos.request(); + } else if (widget.mediaType == MediaType.video) { + permissionStatus = await Permission.videos.request(); + } else { + final statuses = await [Permission.photos, Permission.videos].request(); + permissionStatus = statuses[Permission.photos] ?? statuses[Permission.videos] ?? PermissionStatus.denied; + } + } else if (sdkInt >= 30) { + permissionStatus = await Permission.storage.request(); + } else { + permissionStatus = await Permission.storage.request(); + } + + if (permissionStatus.isGranted) { + final remaining = widget.maxCount - _mediaPaths.length; + final List? assets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, + maxAssets: remaining, + gridCount: 4, + ), + ); + + if (assets != null) { + for (final asset in assets) { + if (_mediaPaths.length >= widget.maxCount) break; + try { + final file = await asset.file; + if (file != null) { + final path = file.path; + await _handlePickedPath(path); + } else { + debugPrint('资产获取 file 为空,asset id: ${asset.id}'); + } + } catch (e) { + debugPrint('读取 asset 文件失败: $e'); + } + } + if (mounted) setState(() {}); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } else if (permissionStatus.isPermanentlyDenied) { + if (mounted) { + ToastUtil.showNormal(context, '请到设置中开启相册访问权限'); + } + await openAppSettings(); + return; + } else { + if (mounted) { + ToastUtil.showNormal(context, '相册访问权限被拒绝'); + } + return; + } + } + } catch (e, st) { + debugPrint('相册选择失败: $e\n$st'); + if (mounted) { + ToastUtil.showNormal(context, '相册选择失败'); + } + } + } + + Future _cameraAction() async { + if (!widget.isEdit || _mediaPaths.length >= widget.maxCount) return; + + final PermissionStatus status = await Permission.camera.request(); + + if (status != PermissionStatus.granted) { + if (mounted) { + ToastUtil.showNormal(context, '相机权限被拒绝'); + } + return; + } + try { + if (widget.mediaType == MediaType.image) { + XFile? picked = await _picker.pickImage(source: ImageSource.camera); + if (picked != null) { + final path = picked.path; + setState(() => _mediaPaths.add(path)); + try { + _localExistsCache[path] = File(path).existsSync(); + } catch (_) { + _localExistsCache[path] = false; + } + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(path); + } + } else { + XFile? picked = await _picker.pickVideo(source: ImageSource.camera); + if (picked != null) { + await _handlePickedPath(picked.path); + } + } + } catch (e) { + debugPrint('拍摄失败: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('拍摄失败,请检查相机权限或设备状态'))); + } + } + } + + void _removeMedia(int index) async { + final ok = await CustomAlertDialog.showConfirm( + context, + title: '温馨提示', + content: widget.mediaType == MediaType.image ? '确定要删除这张图片吗?' : '确定要删除这个视频吗?', + cancelText: '取消', + ); + if (!ok) return; + + final removed = _mediaPaths[index]; + + setState(() => _mediaPaths.removeAt(index)); + + // 从缓存中移除 + _localExistsCache.remove(removed); + + // 始终通知 onMediaRemoved(用于父端业务逻辑) + widget.onMediaRemoved?.call(removed); + widget.onMediaRemovedForIndex?.call(index); + // 只有当本地文件集合发生变化时才触发 onChanged(避免因为删除网络路径导致父端把列表置空) + final localFiles = _localFilesFromPaths(_mediaPaths); + widget.onChanged(localFiles); + } + + @override + Widget build(BuildContext context) { + final showAddButton = widget.isEdit && _mediaPaths.length < widget.maxCount; + final itemCount = _mediaPaths.length + (showAddButton ? 1 : 0); + + // 使用 LayoutBuilder 获取父容器宽度,然后按 crossAxisCount 计算每个 tile 的逻辑宽度 + return LayoutBuilder(builder: (context, constraints) { + final maxWidth = (constraints.maxWidth.isFinite && constraints.maxWidth > 0) + ? constraints.maxWidth + : MediaQuery.of(context).size.width; + final tileLogicalW = (maxWidth / widget.crossAxisCount).round(); + final cacheWidth = (tileLogicalW * MediaQuery.of(context).devicePixelRatio).round(); + + return Stack( + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: 1, + ), + itemCount: itemCount, + itemBuilder: (context, index) { + if (index < _mediaPaths.length) { + final raw = (_mediaPaths[index] ?? '').toString().trim(); + final isNetwork = _isNetworkPath(raw); + + return GestureDetector( + onTap: () => widget.onMediaTapped?.call(raw), + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: SizedBox.expand( + child: raw.isEmpty + ? Container( + color: Colors.grey.shade200, + child: const Center(child: Icon(Icons.broken_image, size: 28, color: Colors.grey)), + ) + : (widget.mediaType == MediaType.image + ? (isNetwork + ? Image.network( + raw, + fit: BoxFit.cover, + width: tileLogicalW.toDouble(), + height: tileLogicalW.toDouble(), + errorBuilder: (_, __, ___) => Container( + color: Colors.grey.shade200, + child: const Center(child: Icon(Icons.broken_image)), + ), + ) + : Image.file( + File(raw), + fit: BoxFit.cover, + width: tileLogicalW.toDouble(), + height: tileLogicalW.toDouble(), + cacheWidth: cacheWidth, + errorBuilder: (_, __, ___) => Container( + color: Colors.grey.shade200, + child: const Center(child: Icon(Icons.broken_image)), + ), + )) + : Container( + color: Colors.black12, + child: const Center( + child: Icon(Icons.videocam, color: Colors.white70), + ), + )), + ), + ), + if (widget.isEdit) + Positioned( + top: -15, + right: -15, + child: IconButton( + icon: const Icon(Icons.cancel, size: 20, color: Colors.red), + onPressed: () => _removeMedia(index), + ), + ), + ], + ), + ); + } else if (showAddButton) { + return GestureDetector( + onTap: widget.isCamera ? _cameraAction : _showPickerOptions, + child: Container( + decoration: BoxDecoration(border: Border.all(color: Colors.black12), borderRadius: BorderRadius.circular(5)), + child: const Center(child: Icon(Icons.camera_alt, color: Colors.black26)), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + if (_isProcessing) + Positioned.fill( + child: Container( + color: Colors.transparent, + child: const Center(child: CircularProgressIndicator()), + ), + ), + ], + ); + }); + } +} + + + +/// ---------- RepairedPhotoSection ---------- +// ------------------ 修改后的 RepairedPhotoSection(完整类) ------------------ +class RepairedPhotoSection extends StatefulWidget { + final int maxCount; + final MediaType mediaType; + final String title; + final List? initialMediaPaths; + final ValueChanged> onChanged; + final ValueChanged? onMediaAdded; + final ValueChanged? onMediaRemoved; + final ValueChanged? onMediaRemovedForIndex; + + final ValueChanged? onMediaTapped; + final bool isFaceImage; + final VoidCallback onAiIdentify; + final bool isShowAI; + final double horizontalPadding; + final bool isRequired; + final bool isShowNum; + final bool isEdit; + final bool isCamera; + final String sectionKey; + final bool followInitialUpdates; + + /// 新增:inlineSingle = true -> 标题和照片在同一行,且只能上传 1 张 + final bool inlineSingle; + /// 可选:当 inlineSingle 为 true 时,可以定制缩略图宽度(px) + final double inlineImageWidth; + + const RepairedPhotoSection({ + Key? key, + this.maxCount = 4, + this.mediaType = MediaType.image, + required this.title, + this.initialMediaPaths, + this.isShowAI = false, + required this.onChanged, + required this.onAiIdentify, + this.isFaceImage = false, + this.horizontalPadding = 5, + this.onMediaAdded, + this.onMediaRemoved, + this.onMediaRemovedForIndex, + this.onMediaTapped, + this.isRequired = false, + this.isShowNum = true, + this.isEdit = true, + this.isCamera = false, + this.followInitialUpdates = false, // 默认 false + this.sectionKey = kAcceptVideoSectionKey, + this.inlineSingle = false, + this.inlineImageWidth = 88.0, + }) : super(key: key); + + @override + _RepairedPhotoSectionState createState() => _RepairedPhotoSectionState(); +} + +class _RepairedPhotoSectionState extends State { + late List _mediaPaths; + StreamSubscription? _sub; + + @override + void initState() { + super.initState(); + _mediaPaths = _normalizePaths(widget.initialMediaPaths).take(widget.maxCount).toList(); + + // 订阅 MediaBus(如果需要) + _sub = MediaBus().stream.listen((ev) { + if (ev.key != widget.sectionKey) return; + + if (ev.type == MediaEventType.clear) { + if (_mediaPaths.isNotEmpty) { + setState(() { + _mediaPaths = []; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } else if (ev.type == MediaEventType.set && ev.paths != null) { + final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList(); + if (!listEquals(newList, _mediaPaths)) { + setState(() { + _mediaPaths = newList; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } + }); + } + + @override + void didUpdateWidget(covariant RepairedPhotoSection oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.followInitialUpdates) { + final oldList = _normalizePaths(oldWidget.initialMediaPaths); + final newList = _normalizePaths(widget.initialMediaPaths); + + if (!listEquals(oldList, newList)) { + setState(() { + _mediaPaths = newList.take(widget.maxCount).toList(); + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } + if (oldWidget.sectionKey != widget.sectionKey) { + _sub?.cancel(); + _sub = MediaBus().stream.listen((ev) { + if (ev.key != widget.sectionKey) return; + + if (ev.type == MediaEventType.clear) { + if (_mediaPaths.isNotEmpty) { + setState(() { + _mediaPaths = []; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } else if (ev.type == MediaEventType.set && ev.paths != null) { + final newList = _normalizePaths(ev.paths).take(widget.maxCount).toList(); + if (!listEquals(newList, _mediaPaths)) { + setState(() { + _mediaPaths = newList; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + } + } + }); + } + } + + @override + void dispose() { + _sub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // 如果要求 inlineSingle 样式:标题与单张照片在同一行 + if (widget.inlineSingle) { + final displayPath = _mediaPaths.isNotEmpty ? _mediaPaths.first : ''; + final isNetwork = _isNetworkPath(displayPath); + + return Container( + color: Colors.white, + padding: EdgeInsets.only(left: widget.horizontalPadding, right: widget.horizontalPadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 标题(左) + Expanded( + child: ListItemFactory.createRowSpaceBetweenItem( + leftText: widget.title, + rightText: '', // inline 时不显示数量 + isRequired: widget.isRequired, + ), + ), + + // 右侧 单张 缩略图 / 上传按钮 + SizedBox( + width: widget.inlineImageWidth, + height: widget.inlineImageWidth, + child: GestureDetector( + onTap: () async { + if (widget.isEdit) { + // 打开 MediaPickerRow 的选择逻辑:我们通过弹出一个底部sheet 让用户拍照或选图 + // 复用 MediaPickerRow 的选择入口:这里直接展示同样的选项 + if (widget.isFaceImage) { + final filePath = await pushPage( + FaceRecognitionPage( + studentId: '', + data: {}, + mode: FaceMode.initSave, + ), + context, + ); + setState(() { + _mediaPaths = [filePath]; + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + + }); + }else{ + showModalBottomSheet( + context: context, + builder: (ctx) => SafeArea( + child: Wrap( + children: [ + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.camera_alt : Icons.videocam), + title: Text(widget.mediaType == MediaType.image ? '拍照' : '拍摄视频'), + onTap: () async { + Navigator.of(ctx).pop(); + // 触发 MediaPickerRow 中的 camera 逻辑:我们在这里简单复用 ImagePicker + final picker = ImagePicker(); + try { + if (widget.mediaType == MediaType.image) { + final x = await picker.pickImage(source: ImageSource.camera); + if (x != null) { + setState(() { + _mediaPaths = [x.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(x.path); + } + } else { + final x = await picker.pickVideo(source: ImageSource.camera); + if (x != null) { + // 若需要转码、压缩请复用 VideoCompress + setState(() { + _mediaPaths = [x.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(x.path); + } + } + } catch (e) { + debugPrint('camera pick error: $e'); + ToastUtil.showNormal(context, '拍摄失败'); + } + }, + ), + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: Icon(widget.mediaType == MediaType.image ? Icons.photo_library : Icons.video_library), + title: Text(widget.mediaType == MediaType.image ? '从相册选择' : '从相册选择视频'), + onTap: () async { + Navigator.of(ctx).pop(); + // 这里直接调用 AssetPicker(与 MediaPickerRow 的行为保持一致) + try { + final List? assets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + requestType: widget.mediaType == MediaType.image ? RequestType.image : RequestType.video, + maxAssets: 1, + gridCount: 4, + ), + ); + if (assets != null && assets.isNotEmpty) { + final file = await assets.first.file; + if (file != null) { + setState(() { + _mediaPaths = [file.path]; + }); + widget.onChanged(_localFilesFromPaths(_mediaPaths)); + widget.onMediaAdded?.call(file.path); + } + } + } catch (e) { + debugPrint('pick asset error: $e'); + ToastUtil.showNormal(context, '选择图片失败'); + } + }, + ), + ListTile( + titleAlignment: ListTileTitleAlignment.center, + leading: const Icon(Icons.close), + title: const Text('取消'), + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ); + + } + } else { + // 非编辑模式:触发查看 + presentOpaque(SingleImageViewer(imageUrl: displayPath), context); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Container( + color: Colors.grey.shade100, + child: displayPath.isEmpty + ? Center(child: Icon(Icons.camera_alt, color: Colors.black26)) + : (widget.mediaType == MediaType.image + ? (isNetwork + ? Image.network(displayPath, fit: BoxFit.cover) + : Image.file(File(displayPath), fit: BoxFit.cover)) + : Stack( + children: [ + Container(color: Colors.black12), + const Center(child: Icon(Icons.videocam, color: Colors.white70)), + ], + )), + ), + ), + ), + ), + + const SizedBox(width: 8), + ], + ), + ); + } + + // 原始布局(title 在上,网格在下)——保持不变 + return Container( + color: Colors.white, + padding: EdgeInsets.only(left: 0, right: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), + child: ListItemFactory.createRowSpaceBetweenItem( + leftText: widget.title, + rightText: widget.isShowNum ? '${_mediaPaths.length}/${widget.maxCount}' : '', + isRequired: widget.isRequired, + ), + ), + const SizedBox(height: 8), + Padding( + padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), + child: MediaPickerRow( + maxCount: widget.maxCount, + mediaType: widget.mediaType, + initialMediaPaths: _mediaPaths, + onMediaRemovedForIndex: widget.onMediaRemovedForIndex, + isCamera: widget.isCamera, + onChanged: (files) { + final newPaths = files.map((f) => f.path).toList(); + setState(() { + _mediaPaths = newPaths; + }); + widget.onChanged(files); + }, + onMediaAdded: widget.onMediaAdded, + onMediaRemoved: widget.onMediaRemoved, + onMediaTapped: (filePath) { + if (widget.mediaType == MediaType.image) { + presentOpaque(SingleImageViewer(imageUrl: filePath), context); + } else { + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (_) => VideoPlayerPopup(videoUrl: filePath), + ); + } + }, + isEdit: widget.isEdit, + ), + ), + const SizedBox(height: 8), + if (widget.isShowAI && widget.isEdit) + Padding( + padding: EdgeInsets.symmetric(horizontal: widget.horizontalPadding), + child: GestureDetector( + onTap: widget.onAiIdentify, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFDFEAFF), + borderRadius: BorderRadius.circular(18), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/images/ai_img.png', width: 20), + const SizedBox(width: 5), + const Text('AI隐患识别与处理'), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + diff --git a/lib/customWidget/picker/CupertinoDatePicker.dart b/lib/customWidget/picker/CupertinoDatePicker.dart new file mode 100644 index 0000000..a43fc6d --- /dev/null +++ b/lib/customWidget/picker/CupertinoDatePicker.dart @@ -0,0 +1,549 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// 调用示例: +/// DateTime? picked = await BottomDateTimePicker.showDate( +/// context, +/// mode: BottomPickerMode.date, // 或 BottomPickerMode.dateTime(默认)或 BottomPickerMode.dateTimeWithSeconds +/// allowFuture: true, +/// allowPast: false, // 是否允许选择过去(false 表示只能选择现在或未来) +/// minTimeStr: '2025-08-20 08:30:45', +/// ); +/// if (picked != null) { +/// print('用户选择的时间:$picked'); +/// } +enum BottomPickerMode { + dateTime, // 底部弹窗 年月日时分 + date, // 中间弹窗日历 + dateTimeWithSeconds, // 底部弹窗 年月日时分秒 +} + +class BottomDateTimePicker { + static Future showDate( + BuildContext context, { + bool allowFuture = true, + bool allowPast = true, // 是否允许选择过去(默认允许) + String? minTimeStr, // 可选:'yyyy-MM-dd HH:mm:ss' + BottomPickerMode mode = BottomPickerMode.dateTime, + }) { + return showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(12)), + ), + builder: + (_) => _InlineDateTimePickerContent( + allowFuture: allowFuture, + allowPast: allowPast, + minTimeStr: minTimeStr, + mode: mode, + ), + ); + } +} + +class _InlineDateTimePickerContent extends StatefulWidget { + final bool allowFuture; + final bool allowPast; + final String? minTimeStr; + final BottomPickerMode mode; + + const _InlineDateTimePickerContent({ + Key? key, + this.allowFuture = true, + this.allowPast = true, + this.minTimeStr, + this.mode = BottomPickerMode.dateTime, + }) : super(key: key); + + @override + State<_InlineDateTimePickerContent> createState() => + _InlineDateTimePickerContentState(); +} + +class _InlineDateTimePickerContentState + extends State<_InlineDateTimePickerContent> { + // 数据源 + final List years = List.generate(101, (i) => 1970 + i); + final List months = List.generate(12, (i) => i + 1); + final List hours = List.generate(24, (i) => i); + final List minutes = List.generate(60, (i) => i); + final List seconds = List.generate(60, (i) => i); // 新增秒数据源 + + // 动态天数列表(根据年月变化) + late List days; + + // Controllers + late FixedExtentScrollController yearCtrl; + late FixedExtentScrollController monthCtrl; + late FixedExtentScrollController dayCtrl; + late FixedExtentScrollController hourCtrl; + late FixedExtentScrollController minuteCtrl; + late FixedExtentScrollController secondCtrl; // 新增秒控制器 + + // 当前选中值 + late int selectedYear; + late int selectedMonth; + late int selectedDay; + late int selectedHour; + late int selectedMinute; + late int selectedSecond; // 新增秒选中值 + + DateTime? _minTime; // 解析后的最小允许时间(如果有) + + @override + void initState() { + super.initState(); + + // 解析 minTimeStr(若提供) + _minTime = _parseMinTime(widget.minTimeStr); + + // 初始时间:取 now 与 _minTime 的较大者(但要考虑 allowPast) + final now = DateTime.now(); + DateTime initial = now; + + // 如果指定了最小时间并且比 now 晚,则以最小时间为初始 + if (_minTime != null && _minTime!.isAfter(initial)) { + initial = _minTime!; + } + + // 如果不允许选择过去,则确保 initial 至少为 now(或当天的 00:00,取决于模式) + if (!widget.allowPast) { + if (widget.mode == BottomPickerMode.date) { + final today = DateTime(now.year, now.month, now.day); + if (initial.isBefore(today)) initial = today; + } else { + if (initial.isBefore(now)) initial = now; + } + } + + // 根据模式调整初始值 + if (widget.mode == BottomPickerMode.date) { + initial = DateTime(initial.year, initial.month, initial.day); + } else if (widget.mode == BottomPickerMode.dateTime) { + initial = DateTime( + initial.year, + initial.month, + initial.day, + initial.hour, + initial.minute, + ); + } + // dateTimeWithSeconds 模式保持完整的时间 + + selectedYear = initial.year; + selectedMonth = initial.month; + selectedDay = initial.day; + selectedHour = initial.hour; + selectedMinute = initial.minute; + selectedSecond = initial.second; + + // 初始化天数列表 + days = _getDaysInMonth(selectedYear, selectedMonth); + + // controllers 初始项索引需在范围内 + yearCtrl = FixedExtentScrollController( + initialItem: years.indexOf(selectedYear).clamp(0, years.length - 1), + ); + monthCtrl = FixedExtentScrollController( + initialItem: (selectedMonth - 1).clamp(0, months.length - 1), + ); + dayCtrl = FixedExtentScrollController( + initialItem: (selectedDay - 1).clamp(0, days.length - 1), + ); + hourCtrl = FixedExtentScrollController( + initialItem: selectedHour.clamp(0, hours.length - 1), + ); + minuteCtrl = FixedExtentScrollController( + initialItem: selectedMinute.clamp(0, minutes.length - 1), + ); + secondCtrl = FixedExtentScrollController( + // 初始化秒控制器 + initialItem: selectedSecond.clamp(0, seconds.length - 1), + ); + + // 确保初始选择满足约束(例如 minTime 或禁止未来/禁止过去) + WidgetsBinding.instance.addPostFrameCallback((_) { + _enforceConstraintsAndUpdateControllers(); + }); + } + + // 根据年月获取当月天数 + List _getDaysInMonth(int year, int month) { + final lastDay = DateUtils.getDaysInMonth(year, month); + return List.generate(lastDay, (i) => i + 1); + } + + // 解析 'yyyy-MM-dd HH:mm:ss' 返回 DateTime 或 null + DateTime? _parseMinTime(String? s) { + if (s == null || s.trim().isEmpty) return null; + try { + final trimmed = s.trim(); + final parts = trimmed.split(' '); + final dateParts = parts[0].split('-').map((e) => int.parse(e)).toList(); + final timeParts = + (parts.length > 1) + ? parts[1].split(':').map((e) => int.parse(e)).toList() + : [0, 0, 0]; + final year = dateParts[0]; + final month = dateParts[1]; + final day = dateParts[2]; + final hour = (timeParts.isNotEmpty) ? timeParts[0] : 0; + final minute = (timeParts.length > 1) ? timeParts[1] : 0; + final second = (timeParts.length > 2) ? timeParts[2] : 0; + return DateTime(year, month, day, hour, minute, second); + } catch (e) { + debugPrint('parseMinTime failed for "$s": $e'); + return null; + } + } + + // 更新天数列表并调整选中日期 + void _updateDays({bool jumpDay = true}) { + final newDays = _getDaysInMonth(selectedYear, selectedMonth); + final isDayValid = selectedDay <= newDays.length; + + setState(() { + days = newDays; + if (!isDayValid) { + selectedDay = newDays.last; + if (jumpDay) dayCtrl.jumpToItem(selectedDay - 1); + } + }); + } + + // 检查并限制时间(模式感知),支持 allowPast 与 allowFuture + void _enforceConstraintsAndUpdateControllers() { + final now = DateTime.now(); + final isDateOnly = widget.mode == BottomPickerMode.date; + final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime; + + DateTime picked; + if (isDateOnly) { + picked = DateTime(selectedYear, selectedMonth, selectedDay); + } else if (isDateTimeOnly) { + picked = DateTime( + selectedYear, + selectedMonth, + selectedDay, + selectedHour, + selectedMinute, + ); + } else { + picked = DateTime( + selectedYear, + selectedMonth, + selectedDay, + selectedHour, + selectedMinute, + selectedSecond, + ); + } + + // 处理最小时间约束:结合 _minTime 与 allowPast + DateTime? minRef; + if (!widget.allowPast) { + // 不允许选择过去:最小时间至少为 now(或当天 00:00) + if (isDateOnly) { + minRef = DateTime(now.year, now.month, now.day); + } else if (isDateTimeOnly) { + minRef = DateTime(now.year, now.month, now.day, now.hour, now.minute); + } else { + minRef = now; + } + // 如果用户也指定了 _minTime 且比 now 晚,则以 _minTime 为准 + if (_minTime != null && _minTime!.isAfter(minRef)) { + minRef = _minTime; + // 根据模式调整精度 + if (isDateOnly) { + minRef = DateTime(minRef!.year, minRef.month, minRef.day); + } else if (isDateTimeOnly) { + minRef = DateTime( + minRef!.year, + minRef.month, + minRef.day, + minRef.hour, + minRef.minute, + ); + } + } + } else if (_minTime != null) { + // 允许选择过去,但若指定了 _minTime,则以 _minTime 为最小参考 + minRef = _minTime; + // 根据模式调整精度 + if (isDateOnly) { + minRef = DateTime(minRef!.year, minRef.month, minRef.day); + } else if (isDateTimeOnly) { + minRef = DateTime( + minRef!.year, + minRef.month, + minRef.day, + minRef.hour, + minRef.minute, + ); + } + } + + if (minRef != null && picked.isBefore(minRef)) { + // 把选中项调整为 minRef + selectedYear = minRef.year; + selectedMonth = minRef.month; + selectedDay = minRef.day; + if (!isDateOnly) { + selectedHour = minRef.hour; + selectedMinute = minRef.minute; + if (!isDateTimeOnly) { + selectedSecond = minRef.second; + } else { + selectedSecond = 0; + } + } else { + selectedHour = 0; + selectedMinute = 0; + selectedSecond = 0; + } + + _updateDays(jumpDay: false); + yearCtrl.jumpToItem(years.indexOf(selectedYear)); + monthCtrl.jumpToItem(selectedMonth - 1); + dayCtrl.jumpToItem(selectedDay - 1); + if (!isDateOnly) { + hourCtrl.jumpToItem(selectedHour); + minuteCtrl.jumpToItem(selectedMinute); + if (!isDateTimeOnly) { + secondCtrl.jumpToItem(selectedSecond); + } + } + return; + } + + // 处理禁止选择未来(当 allowFuture == false) + if (!widget.allowFuture) { + DateTime nowRef; + if (isDateOnly) { + nowRef = DateTime(now.year, now.month, now.day); + } else if (isDateTimeOnly) { + nowRef = DateTime(now.year, now.month, now.day, now.hour, now.minute); + } else { + nowRef = now; + } + + if (picked.isAfter(nowRef)) { + selectedYear = nowRef.year; + selectedMonth = nowRef.month; + selectedDay = nowRef.day; + if (!isDateOnly) { + selectedHour = nowRef.hour; + selectedMinute = nowRef.minute; + if (!isDateTimeOnly) { + selectedSecond = nowRef.second; + } else { + selectedSecond = 0; + } + } else { + selectedHour = 0; + selectedMinute = 0; + selectedSecond = 0; + } + + _updateDays(jumpDay: false); + yearCtrl.jumpToItem(years.indexOf(selectedYear)); + monthCtrl.jumpToItem(selectedMonth - 1); + dayCtrl.jumpToItem(selectedDay - 1); + if (!isDateOnly) { + hourCtrl.jumpToItem(selectedHour); + minuteCtrl.jumpToItem(selectedMinute); + if (!isDateTimeOnly) { + secondCtrl.jumpToItem(selectedSecond); + } + } + return; + } + } + } + + @override + void dispose() { + yearCtrl.dispose(); + monthCtrl.dispose(); + dayCtrl.dispose(); + hourCtrl.dispose(); + minuteCtrl.dispose(); + secondCtrl.dispose(); // 释放秒控制器 + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isDateOnly = widget.mode == BottomPickerMode.date; + final isDateTimeOnly = widget.mode == BottomPickerMode.dateTime; + + // 根据模式计算高度 + final height = isDateOnly ? 280 : (isDateTimeOnly ? 330 : 380); + + return SizedBox( + height: height.toDouble(), + child: Column( + children: [ + // 顶部按钮 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("取消", style: TextStyle(color: Colors.grey)), + ), + TextButton( + onPressed: () { + DateTime result; + if (isDateOnly) { + result = DateTime( + selectedYear, + selectedMonth, + selectedDay, + ); + } else if (isDateTimeOnly) { + result = DateTime( + selectedYear, + selectedMonth, + selectedDay, + selectedHour, + selectedMinute, + ); + } else { + result = DateTime( + selectedYear, + selectedMonth, + selectedDay, + selectedHour, + selectedMinute, + selectedSecond, + ); + } + Navigator.of(context).pop(result); + }, + child: const Text("确定", style: TextStyle(color: Colors.blue)), + ), + ], + ), + ), + const Divider(height: 1), + + // 可见的滚轮列 + Expanded( + child: Row( + children: [ + // 年 + _buildPicker( + controller: yearCtrl, + items: years.map((e) => e.toString()).toList(), + onSelected: (idx) { + setState(() { + selectedYear = years[idx]; + _updateDays(); + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + + // 月 + _buildPicker( + controller: monthCtrl, + items: + months.map((e) => e.toString().padLeft(2, '0')).toList(), + onSelected: (idx) { + setState(() { + selectedMonth = months[idx]; + _updateDays(); + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + + // 日 + _buildPicker( + controller: dayCtrl, + items: days.map((e) => e.toString().padLeft(2, '0')).toList(), + onSelected: (idx) { + setState(() { + final safeIdx = idx.clamp(0, days.length - 1); + selectedDay = days[safeIdx]; + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + + // 若不是 dateOnly,则显示时分两列 + if (!isDateOnly) + _buildPicker( + controller: hourCtrl, + items: + hours.map((e) => e.toString().padLeft(2, '0')).toList(), + onSelected: (idx) { + setState(() { + selectedHour = hours[idx]; + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + + if (!isDateOnly) + _buildPicker( + controller: minuteCtrl, + items: + minutes + .map((e) => e.toString().padLeft(2, '0')) + .toList(), + onSelected: (idx) { + setState(() { + selectedMinute = minutes[idx]; + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + + // 如果是 dateTimeWithSeconds 模式,显示秒列 + if (widget.mode == BottomPickerMode.dateTimeWithSeconds) + _buildPicker( + controller: secondCtrl, + items: + seconds + .map((e) => e.toString().padLeft(2, '0')) + .toList(), + onSelected: (idx) { + setState(() { + selectedSecond = seconds[idx]; + _enforceConstraintsAndUpdateControllers(); + }); + }, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPicker({ + required FixedExtentScrollController controller, + required List items, + required ValueChanged onSelected, + }) { + return Expanded( + child: CupertinoPicker.builder( + scrollController: controller, + itemExtent: 40, + childCount: items.length, + onSelectedItemChanged: onSelected, + itemBuilder: (context, index) { + return Center(child: Text(items[index])); + }, + ), + ); + } +} diff --git a/lib/customWidget/remote_file_page.dart b/lib/customWidget/remote_file_page.dart new file mode 100644 index 0000000..53ea227 --- /dev/null +++ b/lib/customWidget/remote_file_page.dart @@ -0,0 +1,262 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:pdfx/pdfx.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class RemoteFilePage extends StatefulWidget { + final String fileUrl; + final int countdownSeconds; + + const RemoteFilePage({ + Key? key, + required this.fileUrl, + this.countdownSeconds = 3, + }) : super(key: key); + + @override + _RemoteFilePageState createState() => _RemoteFilePageState(); +} + +class _RemoteFilePageState extends State { + String? _localPath; + bool _isLoading = true; + bool _hasScrolledToBottom = false; + bool _timerFinished = false; + late int _secondsRemaining; + Timer? _countdownTimer; + PdfControllerPinch? _pdfController; + int _totalPages = 0; + + // ========== 新增用于方案1的字段 ========== + Size? _viewportSize; // 当前 viewport 大小,用于计算 scale + final List _pageTopOffsets = []; // 每页顶部偏移(相对于内容起点) + double _totalContentHeight = 0; // 计算得到的滚动内容总高度 + double _pageSpacing = 0.0; // 如果你在 PdfViewPinch 布局里有页间距,可设置 + // ====================================== + + @override + void initState() { + super.initState(); + _secondsRemaining = widget.countdownSeconds; + _startCountdown(); + _downloadAndLoad(); + LoadingDialogHelper.hide(); + } + + Future _downloadAndLoad() async { + try { + final url = widget.fileUrl; + final filename = url.split('/').last; + final dir = await getTemporaryDirectory(); + final filePath = '${dir.path}/$filename'; + + final dio = Dio(); + final response = await dio.get>( + url, + options: Options(responseType: ResponseType.bytes), + ); + + final file = File(filePath); + await file.writeAsBytes(response.data!); + + // 直接传 Future 给 controller(不要 await) + final futureDoc = PdfDocument.openFile(filePath); // Future + _pdfController = PdfControllerPinch(document: futureDoc); + + // 注册 pageListenable listener(pageListenable 是 ValueListenable,1-based) + _pdfController!.pageListenable.addListener(_onPageListenableChanged); + + if (!mounted) return; + setState(() { + _localPath = filePath; + _isLoading = false; + }); + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + ToastUtil.showNormal(context, '文件加载失败: $e'); + } + } + } + + void _onPageListenableChanged() { + // pageListenable.value 是当前页(1-based) + final controller = _pdfController; + if (controller == null) return; + final currentPage = controller.pageListenable.value; + // 如果 totalPages 已知且到达最后一页,则标记为已看完 + if (_totalPages > 0 && currentPage >= _totalPages - 3) { + if (!_hasScrolledToBottom && mounted) { + setState(() { + _hasScrolledToBottom = true; + }); + } + } + } + + void _startCountdown() { + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) return; + setState(() { + if (_secondsRemaining > 1) { + _secondsRemaining--; + } else { + _secondsRemaining = 0; + _timerFinished = true; + _countdownTimer?.cancel(); + } + }); + }); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + // 移除 listener 再释放 controller(注意 controller 和其 listenable 可能为 null) + try { + if (_pdfController != null) { + _pdfController!.pageListenable.removeListener(_onPageListenableChanged); + _pdfController!.dispose(); + } + } catch (e) { + // ignore dispose errors + } + super.dispose(); + } + + // 当滚动通知到达底部(或接近底部)时调用 — 作为备选方案/调试用 + bool _handleScrollNotification(ScrollNotification notification) { + final metrics = notification.metrics; + + // 优先使用我们计算的 totalContentHeight + viewport 判断(更准确) + if (_totalContentHeight > 0 && _viewportSize != null) { + final viewportH = _viewportSize!.height; + final threshold = math.min(50.0, viewportH * 0.1); // 灵活阈值 + final scrolledToBottom = (metrics.pixels + viewportH) >= (_totalContentHeight - threshold); + + if (scrolledToBottom) { + if (!_hasScrolledToBottom && mounted) { + setState(() { + _hasScrolledToBottom = true; + }); + } + } + } else { + // 兜底:使用 maxScrollExtent 判断(保留你原来的逻辑) + if ((metrics.maxScrollExtent - metrics.pixels) <= 5.0 || + (metrics.atEdge && metrics.pixels == metrics.maxScrollExtent)) { + if (!_hasScrolledToBottom && mounted) { + setState(() { + _hasScrolledToBottom = true; + }); + } + } + } + return false; // 让事件继续传递 + } + + @override + Widget build(BuildContext context) { + final isButtonEnabled = _timerFinished && _hasScrolledToBottom; + return Scaffold( + appBar: MyAppbar(title: '资料学习'), + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + Expanded( + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : LayoutBuilder(builder: (context, constraints) { + // 保存 viewport size(用于计算 scale) + _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); + + return NotificationListener( + onNotification: _handleScrollNotification, + child: PdfViewPinch( + controller: _pdfController!, + scrollDirection: Axis.vertical, + onDocumentLoaded: (document) async { + // 原有行为:保存页数 + _totalPages = document.pagesCount; + + // ======= 新增:计算每页渲染高度并累加 totalContentHeight ======= + // 注意:document.getPage(i) 后要 close() + try { + final viewportW = _viewportSize?.width ?? MediaQuery.of(context).size.width; + final viewportH = _viewportSize?.height ?? MediaQuery.of(context).size.height; + + _pageTopOffsets.clear(); + double acc = 0; + for (int i = 1; i <= _totalPages; i++) { + final page = await document.getPage(i); + // page.width / page.height 可能是 num/double + final pw = (page.width is num) ? (page.width as num).toDouble() : page.width.toDouble(); + final ph = (page.height is num) ? (page.height as num).toDouble() : page.height.toDouble(); + + // 模拟 contained 缩放行为:scale = min(viewportW / pw, viewportH / ph) + final scale = math.min(viewportW / pw, viewportH / ph); + final renderedH = ph * scale; + + _pageTopOffsets.add(acc); + acc += renderedH + _pageSpacing; + + await page.close(); + } + _totalContentHeight = acc; + } catch (e) { + // 计算失败则保留 _totalContentHeight 为 0,使用兜底判断 + _totalContentHeight = 0; + } + // ============================================================== + + // 保留你原来:如果页少,直接视为已看完 + if (_totalPages <= 3) { + if (!_hasScrolledToBottom && mounted) { + setState(() => _hasScrolledToBottom = true); + } + } + + if (mounted) setState(() {}); + }, + // 作为兜底:当 page 到最后一页时也标记为已看完(注意 page 是 1-based) + onPageChanged: (page) { + if (_totalPages > 0 && page == _totalPages) { + if (!_hasScrolledToBottom && mounted) { + setState(() => _hasScrolledToBottom = true); + } + } + }, + ), + ); + }), + ), + Padding( + padding: const EdgeInsets.all(16), + child: CustomButton( + backgroundColor: isButtonEnabled ? Colors.blue : Colors.grey, + text: isButtonEnabled + ? '我已学习完毕' + : _secondsRemaining == 0 ? '我已学习完毕' : '($_secondsRemaining s)我已学习完毕', + onPressed: isButtonEnabled + ? () { + Navigator.pop(context, true); + } + : null, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/customWidget/search_bar_widget.dart b/lib/customWidget/search_bar_widget.dart new file mode 100644 index 0000000..187c7cf --- /dev/null +++ b/lib/customWidget/search_bar_widget.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +class SearchBarWidget extends StatefulWidget { + final String hintText; + final String buttonText; + final ValueChanged onSearch; + final TextEditingController controller; + final bool showResetButton; + final String resetButtonText; + final VoidCallback? onReset; + final bool isClickableOnly; + final VoidCallback? onInputTap; + final ValueChanged? onTextChanged; + final bool isShowSearchButton; + final double height; // 新增:可配置高度 + + const SearchBarWidget({ + Key? key, + required this.onSearch, + required this.controller, + this.hintText = '请输入关键字', + this.buttonText = '搜索', + this.showResetButton = false, + this.resetButtonText = '重置', + this.onReset, + this.isClickableOnly = false, + this.onInputTap, + this.onTextChanged, + this.isShowSearchButton = true, + this.height = 40.0, // 默认更紧凑的高度 + }) : super(key: key); + + @override + _SearchBarWidgetState createState() => _SearchBarWidgetState(); +} + +class _SearchBarWidgetState extends State { + late FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + /// 更新输入框内容(可在外部调用) + void updateText(String newText) { + widget.controller.text = newText; + widget.controller.selection = TextSelection.fromPosition( + TextPosition(offset: newText.length), + ); + widget.onTextChanged?.call(newText); + } + + @override + Widget build(BuildContext context) { + // 计算 contentPadding,使文本垂直居中 + final innerHeight = widget.height; + // 文本行高大约 20 左右,留出少量上下内边距 + final verticalPadding = (innerHeight - 20).clamp(0.0, innerHeight) / 2; + + return Row( + children: [ + Expanded( + child: SizedBox( + height: innerHeight, + child: TextField( + focusNode: _focusNode, + controller: widget.controller, + readOnly: widget.isClickableOnly, + autofocus: false, + style: const TextStyle(fontSize: 14), + onChanged: widget.onTextChanged, + onTap: () { + if (widget.isClickableOnly) { + widget.onInputTap?.call(); + } + }, + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFFF5F5F5), + prefixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 12), + Image.asset('assets/images/search.png', height: 15, width: 15,) + ], + ), + // 控制 prefixIcon 的约束,保证图标不会撑高 + prefixIconConstraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + hintText: widget.hintText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide.none, + ), + isDense: true, + contentPadding: EdgeInsets.symmetric( + vertical: verticalPadding, + horizontal: 8, + ), + ), + onSubmitted: widget.onSearch, + ), + ), + ), + 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( + 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), + ), + ), + ), + ], + ); + } +} diff --git a/lib/customWidget/single_image_viewer.dart b/lib/customWidget/single_image_viewer.dart new file mode 100644 index 0000000..24fe779 --- /dev/null +++ b/lib/customWidget/single_image_viewer.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +// 查看大图 +class SingleImageViewer extends StatelessWidget { + final String imageUrl; + const SingleImageViewer({Key? key, required this.imageUrl}) : super(key: key); + + @override + Widget build(BuildContext context) { + ImageProvider provider; + + // 选择图片来源:网络 / asset / 本地文件(优先检测 http,然后 asset,再文件) + if (imageUrl.toLowerCase().startsWith('http')) { + provider = NetworkImage(imageUrl); + } else if (imageUrl.startsWith('assets/') || + imageUrl.startsWith('package:') || + imageUrl.startsWith('packages/')) { + // 明确标记为工程资源(asset) + provider = AssetImage(imageUrl) as ImageProvider; + } else { + // 不是明确的网络或 asset 路径 —— 在非 web 平台尝试作为文件路径 + if (!kIsWeb) { + try { + final file = File(imageUrl); + if (file.existsSync()) { + provider = FileImage(file); + } else { + // 文件不存在时尝试作为 asset 路径回退 + provider = AssetImage(imageUrl) as ImageProvider; + } + } catch (e) { + // 任何异常都回退为 asset 尝试(避免抛出) + provider = AssetImage(imageUrl) as ImageProvider; + } + } else { + // web 平台没有 File API,可直接尝试当作 asset + provider = AssetImage(imageUrl) as ImageProvider; + } + } + + return Scaffold( + backgroundColor: Colors.black.withValues(alpha: 0.5), + appBar: MyAppbar( + isBack: false, + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.close, + color: Colors.white, + size: 40, + ), + ) + ], + backgroundColor: Colors.black.withValues(alpha: 0.5), + title: '', + ), + body: Center( + child: PhotoView( + imageProvider: provider, + backgroundDecoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5)), + minScale: PhotoViewComputedScale.contained, + maxScale: PhotoViewComputedScale.covered * 2, + onTapUp: (context, details, controllerValue) { + Navigator.of(context).pop(); + }, + ), + ), + ); + } +} diff --git a/lib/customWidget/toast_util.dart b/lib/customWidget/toast_util.dart new file mode 100644 index 0000000..d42e5f2 --- /dev/null +++ b/lib/customWidget/toast_util.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class ToastUtil { + /// 普通提示(仅文字,屏幕中间) + static void showNormal( + BuildContext context, + String message, { + ToastGravity gravity = ToastGravity.CENTER, // 修改为 CENTER + int duration = 2, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + ); + } + + /// 成功提示(带图标,屏幕中间) + static void showSuccess( + BuildContext context, + String message, { + ToastGravity gravity = ToastGravity.CENTER, // 修改为 CENTER + int duration = 3, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + isSuccess: true, + ); + } + + /// 失败提示(带图标,屏幕中间) + static void showError( + BuildContext context, + String message, { + ToastGravity gravity = ToastGravity.CENTER, // 修改为 CENTER + int duration = 4, + }) { + _showToast( + context: context, + message: message, + gravity: gravity, + duration: duration, + isError: true, + ); + } + + /// 内部通用方法 + static void _showToast({ + required BuildContext context, + required String message, + required ToastGravity gravity, + required int duration, + bool isSuccess = false, + bool isError = false, + }) { + // 如果有图标(成功或失败),则使用自定义 Widget + if (isSuccess || isError) { + final fToast = FToast(); + fToast.init(context); + fToast.showToast( + child: _buildIconToast(message, isSuccess: isSuccess), + gravity: gravity, + toastDuration: Duration(seconds: duration), + ); + } + // 普通文字提示 + else { + Fluttertoast.showToast( + msg: message, + toastLength: duration > 2 ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, + gravity: gravity, // 始终 CENTER + backgroundColor: Colors.grey[500], + textColor: Colors.white, + fontSize: 16.0, + ); + } + } + + /// 构建带图标的 Toast Widget + static Widget _buildIconToast(String message, {bool isSuccess = true}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Colors.grey[850]?.withOpacity(0.9), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isSuccess ? Icons.check : Icons.error_outline, + color: isSuccess ? Colors.greenAccent[400] : Colors.redAccent[400], + size: 36.0, + ), + const SizedBox(height: 8.0), + Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/customWidget/video_player_widget.dart b/lib/customWidget/video_player_widget.dart new file mode 100644 index 0000000..6185658 --- /dev/null +++ b/lib/customWidget/video_player_widget.dart @@ -0,0 +1,477 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPlayerWidget extends StatefulWidget { + final VideoPlayerController? controller; + final String coverUrl; + final double aspectRatio; + final bool allowSeek; + final bool isFullScreen; + + const VideoPlayerWidget({ + Key? key, + required this.controller, + required this.coverUrl, + required this.aspectRatio, + this.allowSeek = true, + this.isFullScreen = false, + }) : super(key: key); + + @override + _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(); +} + +class _VideoPlayerWidgetState extends State { + bool _visibleControls = true; + Timer? _hideTimer; + Timer? _positionTimer; + Duration _currentPosition = Duration.zero; + Duration _totalDuration = Duration.zero; + bool _isPlaying = false; + final ValueNotifier _sliderValue = ValueNotifier(0.0); + + // 判断是否有controller,避免被销毁后访问 + bool get _hasController => widget.controller != null; + + /// 安全判断 controller 是否初始化,避免抛异常 + bool _controllerInitializedSafe() { + try { + final c = widget.controller; + if (c == null) return false; + return c.value.isInitialized; + } catch (e) { + return false; + } + } + + @override + void initState() { + super.initState(); + // 初始化时立即启动隐藏控制栏的定时器 + _startHideTimer(); + // 如果有controller并且初始化了,则启动进度定时器 + _maybeStartPositionTimer(); + // 给controller添加监听 + _addControllerListenerSafely(); + // 如果controller已经初始化,获取初始值 + if (_controllerInitializedSafe()) { + _updateControllerValuesSafe(); + } + } + + @override + void didUpdateWidget(covariant VideoPlayerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + // 如果controller发生变化,需要移除旧的监听,添加新的监听 + if (oldWidget.controller != widget.controller) { + _removeControllerListenerSafely(oldWidget.controller); + _addControllerListenerSafely(); + _restartPositionTimer(); + _updateControllerValuesSafe(); + } + } + + void _addControllerListenerSafely() { + try { + widget.controller?.addListener(_controllerListener); + } catch (_) { + // 忽略已经被销毁的情况 + } + } + + void _removeControllerListenerSafely([VideoPlayerController? ctrl]) { + final c = ctrl ?? widget.controller; + if (c == null) return; + try { + c.removeListener(_controllerListener); + } catch (_) { + // 忽略 + } + } + + void _controllerListener() { + if (!mounted) return; + _updateControllerValuesSafe(); + } + + // 更新播放状态、进度等信息 + void _updateControllerValuesSafe() { + final c = widget.controller; + if (c == null) { + // 没有controller时清空数据 + if (mounted) { + setState(() { + _isPlaying = false; + _totalDuration = Duration.zero; + _currentPosition = Duration.zero; + _sliderValue.value = 0; + }); + } + return; + } + + try { + final value = c.value; + if (!value.isInitialized) { + if (mounted) { + setState(() { + _isPlaying = false; + _totalDuration = Duration.zero; + _currentPosition = Duration.zero; + _sliderValue.value = 0; + }); + } + return; + } + + final pos = value.position; + final dur = value.duration; + final playing = value.isPlaying; + + if (mounted) { + setState(() { + _isPlaying = playing; + _totalDuration = dur; + _currentPosition = pos; + _sliderValue.value = pos.inMilliseconds.toDouble(); + }); + } + } catch (e) { + // controller被销毁时忽略 + } + } + + // 启动隐藏控制栏的定时器(3秒后隐藏) + void _startHideTimer() { + _hideTimer?.cancel(); + _hideTimer = Timer(const Duration(seconds: 3), () { + if (mounted) setState(() => _visibleControls = false); + }); + } + + // 启动进度定时器(定时刷新播放进度) + void _maybeStartPositionTimer() { + if (_positionTimer != null && _positionTimer!.isActive) return; + if (!_hasController) return; + _positionTimer = Timer.periodic(const Duration(milliseconds: 300), (_) { + if (!mounted) return; + if (!_controllerInitializedSafe()) return; + try { + final c = widget.controller!; + final pos = c.value.position; + final dur = c.value.duration; + final playing = c.value.isPlaying; + setState(() { + _currentPosition = pos; + _totalDuration = dur; + _isPlaying = playing; + _sliderValue.value = pos.inMilliseconds.toDouble(); + }); + } catch (_) { + // controller销毁时忽略 + } + }); + } + + void _restartPositionTimer() { + _positionTimer?.cancel(); + _maybeStartPositionTimer(); + } + + void _stopPositionTimer() { + try { + _positionTimer?.cancel(); + } catch (_) {} + _positionTimer = null; + } + + // 点击切换控制栏显示/隐藏 + void _toggleControls() { + setState(() => _visibleControls = !_visibleControls); + if (_visibleControls) + _startHideTimer(); + else + _hideTimer?.cancel(); + } + + // 播放/暂停切换 + void _togglePlayPause() { + final c = widget.controller; + if (c == null) return; + try { + if (_isPlaying) { + c.pause(); + setState(() => _isPlaying = false); + } else { + c.play(); + setState(() => _isPlaying = true); + } + _startHideTimer(); + } catch (_) {} + } + + // 进入全屏播放 + Future _enterFullScreen() async { + try { + // 设置横屏、沉浸式UI + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + await NativeOrientation.setLandscape(); + } catch (_) {} + + // 跳转到全屏页面 + await Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + top: false, + bottom: false, + child: VideoPlayerWidget( + controller: widget.controller, + coverUrl: widget.coverUrl, + aspectRatio: max( + widget.aspectRatio, + MediaQuery.of(ctx).size.width / MediaQuery.of(ctx).size.height, + ), + allowSeek: widget.allowSeek, + isFullScreen: true, + ), + ), + ), + ), + ); + + // 返回后恢复竖屏和UI + try { + await NativeOrientation.setPortrait(); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } catch (_) {} + + if (!mounted) return; + setState(() {}); // 强制刷新,修复退出全屏后UI问题 + } + + @override + void dispose() { + _hideTimer?.cancel(); + _stopPositionTimer(); + _sliderValue.dispose(); + _removeControllerListenerSafely(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final media = MediaQuery.of(context); + final screenW = media.size.width; + final screenH = media.size.height; + + // 非全屏高度:按照宽高比计算,但不超过屏幕一半高度 + final preferredNonFullHeight = min( + screenW / (widget.aspectRatio <= 0 ? (16 / 9) : widget.aspectRatio), + screenH * 0.5, + ); + + final containerW = screenW; + final containerH = widget.isFullScreen ? screenH : preferredNonFullHeight; + + return Center( + child: SizedBox( + width: containerW, + height: containerH, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: _toggleControls, + child: Stack( + fit: StackFit.expand, + children: [ + // 视频画面或封面图 + _buildVideoOrCover(containerW, containerH), + // 全屏模式时左上角显示返回按钮 + if (widget.isFullScreen) + Positioned( + top: MediaQuery.of(context).padding.top + 8, + left: 8, + child: SafeArea( + top: true, + bottom: false, + child: ClipOval( + child: Material( + color: Colors.black38, + child: InkWell( + onTap: () async { + try { + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } catch (_) {} + Navigator.of(context).maybePop(); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back, + color: Colors.white, + size: 22, + ), + ), + ), + ), + ), + ), + ), + // 控制栏 + if (_visibleControls) _buildControls(), + ], + ), + ), + ), + ); + } + + // 构建视频画面或封面图 + Widget _buildVideoOrCover(double containerW, double containerH) { + final c = widget.controller; + + if (c != null) { + try { + if (c.value.isInitialized) { + final vidAspect = + (c.value.size.height > 0) ? (c.value.size.width / c.value.size.height) : widget.aspectRatio; + return Center( + child: AspectRatio( + aspectRatio: vidAspect > 0 ? vidAspect : widget.aspectRatio, + child: VideoPlayer(c), + ), + ); + } + } catch (_) {} + } + + if (widget.coverUrl.isNotEmpty) { + return Image.network( + widget.coverUrl, + width: containerW, + height: containerH, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container(color: Colors.black), + ); + } + + return Container(color: Colors.black); + } + + // 构建底部控制栏 + Widget _buildControls() { + final totalMs = _totalDuration.inMilliseconds.toDouble(); + final sliderMax = totalMs > 0 ? totalMs : 1.0; + final sliderValue = _sliderValue.value.clamp(0.0, sliderMax).toDouble(); + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.7), + Colors.transparent, + ], + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 28, + color: Colors.white, + ), + onPressed: _togglePlayPause, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _sliderValue, + builder: (_, value, __) => SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: Colors.white, + inactiveTrackColor: Colors.white54, + thumbColor: Colors.white, + overlayColor: Colors.white24, + trackHeight: 2, + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8), + ), + child: Slider( + value: sliderValue, + min: 0, + max: sliderMax, + onChanged: (v) { + if (widget.allowSeek && widget.controller != null) { + try { + widget.controller!.seekTo(Duration(milliseconds: v.toInt())); + setState(() => _currentPosition = Duration(milliseconds: v.toInt())); + _sliderValue.value = v; + _startHideTimer(); + } catch (_) {} + } + }, + ), + ), + ), + ), + SizedBox( + width: 110, + child: Text( + '${_formatDuration(_currentPosition)} / ${_formatDuration(_totalDuration)}', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + icon: Icon( + widget.isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen, + size: 28, + color: Colors.white, + ), + onPressed: () { + if (widget.isFullScreen) { + Navigator.of(context).maybePop(); + } else { + _enterFullScreen(); + } + }, + ), + ], + ), + ), + ); + } + + // 格式化时间 + String _formatDuration(Duration d) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final h = d.inHours, m = d.inMinutes.remainder(60), s = d.inSeconds.remainder(60); + if (h > 0) return '${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}'; + return '${twoDigits(m)}:${twoDigits(s)}'; + } +} diff --git a/lib/customWidget/work_tab_icon_grid.dart b/lib/customWidget/work_tab_icon_grid.dart new file mode 100644 index 0000000..2f33877 --- /dev/null +++ b/lib/customWidget/work_tab_icon_grid.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +/// 网格布局组件 +class WorkTabIconGrid extends StatelessWidget { + final List> buttonInfos; + final ValueChanged onItemPressed; + final double leftSpace; + + const WorkTabIconGrid({ + super.key, + required this.buttonInfos, + required this.onItemPressed, + this.leftSpace = 0, + }); + + @override + Widget build(BuildContext context) { + final double screenW = MediaQuery.of(context).size.width; + final double itemW = (screenW - 2 * leftSpace) / 4; + + return Container( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: leftSpace), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: + buttonInfos.length <= 4 + ? Row( + mainAxisAlignment: MainAxisAlignment.start, + children: + buttonInfos.asMap().entries.map((entry) { + final index = entry.key; + final info = entry.value; + return _buildIconButton( + context: context, + iconPath: info['icon'] as String, + label: info['title'] as String, + unreadCount: + int.tryParse(info['unreadCount'].toString()) ?? 0, + onPressed: () => onItemPressed(index), + width: itemW, + ); + }).toList(), + ) + : Wrap( + spacing: 0, + runSpacing: 16, + alignment: WrapAlignment.start, + children: + buttonInfos.asMap().entries.map((entry) { + final index = entry.key; + final info = entry.value; + return _buildIconButton( + context: context, + iconPath: info['icon'] as String, + label: info['title'] as String, + unreadCount: + info['unreadCount'] is String + ? int.tryParse(info['unreadCount']) + : info['unreadCount'] is int + ? info['unreadCount'] + : 0, + onPressed: () => onItemPressed(index), + width: itemW, + ); + }).toList(), + ), + ); + } + + Widget _buildIconButton({ + required BuildContext context, + required String iconPath, + required String label, + required VoidCallback onPressed, + required double width, + int unreadCount = 0, + }) { + return InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: SizedBox( + width: width, + child: Stack( + clipBehavior: Clip.none, + children: [ + Align( + alignment: Alignment.topCenter, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset(iconPath, width: 30, height: 30), + const SizedBox(height: 5), + Text( + label, + style: const TextStyle(fontSize: 13), + textAlign: TextAlign.center, + ), + ], + ), + ), + if (unreadCount > 0) + Positioned( + right: width / 4 - 10, + top: -5, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints( + minWidth: 16, + minHeight: 16, + ), + child: Center( + child: Text( + unreadCount > 999 ? '999+' : '$unreadCount', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + height: 1, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/http/ApiService.dart b/lib/http/ApiService.dart new file mode 100644 index 0000000..3aea3ff --- /dev/null +++ b/lib/http/ApiService.dart @@ -0,0 +1,35 @@ +export 'modules/auth_api.dart'; +export 'modules/file_api.dart'; +export 'modules/basic_info_api.dart'; +export 'modules/hidden_danger_api.dart'; +export 'modules/special_work_api.dart'; + +class ApiService { + /// 是否正式环境 + static final bool isProduct = false; + + /// 登录及其他管理后台接口 + static final String basePath = + isProduct + ? "https://gbs-gateway.qhdsafety.com" + : "http://192.168.20.100:30140"; + + /// 图片文件服务 + static final String baseImgPath = + isProduct + ? "https://jpfz.qhdsafety.com/gbsFileTest/" + : "http://192.168.20.100:9787/mnt/"; //内网图片地址 + + static const publicKey = + '0402df2195296d4062ac85ad766994d73e871b887e18efb9a9a06b4cebc72372869b7da6c347c129dee2b46a0f279ff066b01c76208c2a052af75977c722a2ccee'; + + /// SM2 私钥 + static const privateKey = + '1cfcaab309f614f10d2fed833331b65da75da7682963a6673a9a5d836b6f8c18'; + + /// 平台 + static final clientId = isProduct ? 'XGFZD' : 'xgfzd'; + + /// appKey + static const appKey = '0bb989ecada5470c87635018ece9f327'; +} diff --git a/lib/http/HttpManager.dart b/lib/http/HttpManager.dart new file mode 100644 index 0000000..d3bf7a8 --- /dev/null +++ b/lib/http/HttpManager.dart @@ -0,0 +1,187 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; +import 'package:dio/dio.dart'; + +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +/// 全局接口异常 +class ApiException implements Exception { + final String result; + final String message; + ApiException(this.result, this.message); + + @override + String toString() => 'ApiException($result): $message'; +} + +/// HTTP 方法枚举 +enum Method { get, post, put, delete } + +/// HTTP 管理器 单例 +class HttpManager { + HttpManager._internal() { + _dio = Dio(BaseOptions( + connectTimeout: const Duration(milliseconds: 20000), + receiveTimeout: const Duration(milliseconds: 20000), + headers: { + 'Content-Type': Headers.formUrlEncodedContentType, + }, + )); + _initInterceptors(); + } + + static final HttpManager _instance = HttpManager._internal(); + factory HttpManager() => _instance; + late final Dio _dio; + + // 添加401处理回调 + static VoidCallback? onUnauthorized; + void _initInterceptors() { + _dio.interceptors + ..add(LogInterceptor(request: true, responseBody: true, error: true)) + ..add(InterceptorsWrapper(onError: (err, handler) { + // TODO 暂不处理 + // 捕获401错误 + if (err.response?.statusCode == 401) { + // 触发全局登出回调 + onUnauthorized?.call(); + // 创建自定义异常 + final apiException = ApiException( + '提示', + '您的账号已在其他设备登录,已自动下线' + ); + // 直接抛出业务异常,跳过后续错误处理 + return handler.reject( + DioException( + requestOptions: err.requestOptions, + error: apiException, + response: err.response, + type: DioExceptionType.badResponse, + ), + ); + } + handler.next(err); + })); + } + + /// 通用请求方法,返回完整后台 JSON + Future> request( + String baseUrl, + String path, { + Method method = Method.post, + Map? data, + Map? params, + CancelToken? cancelToken, + String? contentType, // Content-Type,默认为 jsonContentType + bool isHeartbeat = false, + }) async { + printLongString('参数:${jsonEncode(data)}'); + Response resp; + final url = baseUrl + path; + // 动态 headers,默认使用 jsonContentType + final String contentTypeValue = contentType ?? Headers.jsonContentType; + final headers = { + 'Content-Type': contentTypeValue, + }; + final token = SessionService.instance.token ?? ''; + // final token = 'jjb-saas-auth:oauth:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJjbGllbnRJZFwiOlwieGdmemRcIixcImFjY291bnRJZFwiOjE5OTE2NzQ0MzEzMzY4NDk0MDgsXCJ1c2VyVHlwZUVudW1cIjpcIlBMQVRGT1JNXCIsXCJ1c2VySWRcIjoxOTkxNjc0NDI4MjYxNTMxNjQ4LFwidGVuYW50SWRcIjoxOTkxNjc0NDI4MjYxNTMxNjQ4LFwidGVuYW50TmFtZVwiOlwi5Yas5rOz55u45YWz5pa5XCIsXCJ0ZW5hbnRUeXBlSWRcIjoxOTkwNjkzMzg4MDcyMTI0NDE2LFwidGVuYW50UGFyZW50SWRzXCI6XCIwLDE5ODM3NzMwMTMwODYwNDgyNTYsMTk5MTY3NDQyODI2MTUzMTY0OFwiLFwibmFtZVwiOlwi5Yas5rOz55u45YWz5pa5XCIsXCJhY2Nlc3NUaWNrZXRcIjpcIkg0YXBlMkFaRVcxZFR1OTIwOXNzSDREc3pPWjBoTkZ4eEVlZzRmYTJZaFRVUFA0QkZVZXZmSklhTVdoS1wiLFwicmVmcmVzaFRpY2tldFwiOlwiRlRlZUxIaXJVblhueTBMcXNMcUdyc2dFaGpqVlRRN0pncVptVTBLS0JHVkFCU1ExeENtT3RTWmxRbUdpXCIsXCJleHBpcmVJblwiOjYwNDgwMCxcInJlZnJlc2hFeHBpcmVzSW5cIjo2MDQ4MDAsXCJvcmdJZFwiOjE5OTE2NzQ0MjgyNjE1MzE2NDgsXCJvcmdOYW1lXCI6XCLlhqzms7Pnm7jlhbPmlrlcIixcIm9yZ0lkc1wiOlsxOTkxNjc0NDI4MjYxNTMxNjQ4XSxcInJvbGVzVHlwZXNcIjpbXCJHT1ZfQ0hJTERfQUNDT1VOVFwiXSxcInJvbGVJZHNcIjpbMTk5MDY5MjE3NTA2NjgyNDcwNV0sXCJzY29wZXNcIjpbXSxcInJwY1R5cGVFbnVtXCI6XCJIVFRQXCIsXCJiaW5kTW9iaWxlU2lnblwiOlwiRkFMU0VcIn0iLCJpc3MiOiJwcm8tc2VydmVyIiwiZXhwIjoxNzY1OTU4NDIzfQ.RphPGGnh18RdGZ2vB0-2gKHp6bQg3-rKR4xPvDgH1ek'; + if (token != null && token.isNotEmpty && !isHeartbeat) { + headers['token'] = token; + } + + final options = Options( + method: method.name.toUpperCase(), + contentType: contentTypeValue, + headers: headers, + ); + + try { + switch (method) { + case Method.get: + resp = await _dio.get(url, + queryParameters: {...?params, ...?data}, + cancelToken: cancelToken, + options: options); + break; + case Method.post: + resp = await _dio.post(url, + data: data, + queryParameters: params, + cancelToken: cancelToken, + options: options); + break; + case Method.put: + resp = await _dio.put(url, + data: data, + queryParameters: params, + cancelToken: cancelToken, + options: options); + break; + case Method.delete: + resp = await _dio.delete(url, + queryParameters: params, + cancelToken: cancelToken, + options: options); + break; + } + } on DioException catch (e) { + if (e.error is ApiException) throw e.error as ApiException; + throw ApiException('network_error', e.message ?? e.toString()); + } + + final json = resp.data is Map + ? resp.data as Map + : {}; + return json; + } +} + +/// 上传文件扩展 +extension HttpManagerUpload on HttpManager { + Future> uploadImages({ + required String baseUrl, + required String path, + required Map fromData, + CancelToken? cancelToken, + }) async { + fromData['corpinfoId'] = '1983773013086048256'; + final form = FormData.fromMap(fromData); + + final token = SessionService.instance.token ?? ''; + final headers = { + 'Content-Type': 'multipart/form-data', + }; + if (token.isNotEmpty) { + headers['token'] = token; + } + + try { + final resp = await _dio.post( + baseUrl + path, + data: form, + cancelToken: cancelToken, + options: Options( + method: Method.post.name.toUpperCase(), + // contentType 可以省略或保留,multipart/form-data 会被 Dio 正确处理(boundary 自动添加) + contentType: 'multipart/form-data', + headers: headers, + ), + ); + + final json = resp.data is Map + ? resp.data as Map + : {}; + + return json; + } on DioException catch (e) { + // 如果是我们在拦截器里构造的 ApiException(例如 401),则向上抛出该业务异常 + if (e.error is ApiException) { + throw e.error as ApiException; + } + // 其它情况统一抛出 ApiException,便于上层统一处理 + throw ApiException('network_error', e.message ?? e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/http/modules/auth_api.dart b/lib/http/modules/auth_api.dart new file mode 100644 index 0000000..748f466 --- /dev/null +++ b/lib/http/modules/auth_api.dart @@ -0,0 +1,107 @@ +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 AuthApi { + /// 本地登录 + static Future> userlogin(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/login', + method: Method.post, + data: {...data}, + ); + } + /// 本地获取验证码 + static Future> getUserCaptcha() { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/generateCaptcha', + method: Method.post, + data: {}, + ); + } + /// 底座登录验证接口 + static Future> loginCheck(Map data) { + return HttpManager().request( + ApiService.basePath, + '/login/login', + method: Method.post, + data: {...data}, + ); + } + + /// 获取验证码 + static Future> getCaptcha() { + return HttpManager().request( + ApiService.basePath, + '/login/captcha', + method: Method.get, + data: {}, + ); + } + + /// 心跳 + static Future> heartbeat() { + return HttpManager().request( + ApiService.basePath, + '/base/accounts/token/heartbeat', + method: Method.post, + contentType: Headers.multipartFormDataContentType, + isHeartbeat: true, + data: { + "token": SessionService.instance.token, + }, + ); + } + + /// 获取当前人信息 + static Future> getUserData() { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/user/getInfo', + method: Method.get, + data: {}, + ); + } + + /// 修改密码 + static Future> changePassWord(data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/updatePassword', + method: Method.post, + data: { + ...data, + }, + ); + } + /// 找回密码 + static Future> passwordRecover(data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/passwordRecover', + method: Method.post, + data: { + ...data, + }, + ); + } + /// 上传人脸认证图片 + static Future> reloadMyFace(String imagePath) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/updateUserFaceUrl', + method: Method.post, + data: { + // ...data, + "id": SessionService.instance.accountId ?? "", + "userAvatarUrl": imagePath, + }, + ); + } + + + +} \ No newline at end of file diff --git a/lib/http/modules/basic_info_api.dart b/lib/http/modules/basic_info_api.dart new file mode 100644 index 0000000..da7c3e3 --- /dev/null +++ b/lib/http/modules/basic_info_api.dart @@ -0,0 +1,121 @@ +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; + +class BasicInfoApi { + /// 注册 + static Future> register(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/register', + method: Method.post, + data: {...data}, + ); + } + + /// 发送验证码 + static Future> sendRegisterSms(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/sendPhoneCode', + method: Method.post, + data: {...data}, + ); + } + + /// 完善个人信息 + static Future> updateUserInfo(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/perfectUserInfo', + method: Method.post, + data: {...data}, + ); + } + + // 入职 + static Future> userFirmEntry(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/onboarding', + method: Method.post, + data: {...data}, + ); + } + // 就职信息 + static Future> getEntryInfo(String id) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/app/userCorpRecord/getInfoById/$id', + method: Method.get, + data: {}, + ); + } + /// 获取用户信息 + static Future> getUserMessage(String value) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/user/$value', + method: Method.get, + data: {}, + ); + } + // 问题反馈 + static Future> feedback(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/appuser/problemFeedback', + method: Method.post, + data: {...data}, + ); + } + + /// 获取企业列表 + static Future> getFirmList(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/app/corpInfo/list', + method: Method.post, + data: {...data}, + ); + } + + + + + + + + + + /// 部门树状图 + static Future> getDeptTree(Map data) { + return HttpManager().request( + ApiService.basePath + (ApiService.isProduct ? '/basicInfo' : '/basicInfo') , + '/department/listTree', + method: Method.post, + data: {...data}, + ); + } + + /// 获取部门下所有用户 + static Future> getDeptUsers(final departmentId) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/user/listAll', + method: Method.get, + data: {'departmentId': departmentId}, + ); + } + + /// 数据字典获取 + static Future> getDictValues(String value) { + return HttpManager().request( + ApiService.basePath, + '/config/dict-trees/list/by/dictValues', + method: Method.get, + data: {'appKey': ApiService.appKey, 'dictValue': value}, + ); + } + +} diff --git a/lib/http/modules/file_api.dart b/lib/http/modules/file_api.dart new file mode 100644 index 0000000..f1482dd --- /dev/null +++ b/lib/http/modules/file_api.dart @@ -0,0 +1,110 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/constants/app_enums.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; + +class FileApi { + /// 单文件上传 + static Future> uploadFile( + String imagePath, + UploadFileType fileEnum, + String foreignKey, + ) async { + final file = File(imagePath); + if (!await file.exists()) { + throw ApiException('file_not_found', '图片不存在:$imagePath'); + } + final fileName = file.path.split(Platform.pathSeparator).last; + + + return HttpManager().uploadImages( + baseUrl: ApiService.basePath, + path: '/basicInfo/imgFiles/save', + fromData: { + 'foreignKey': foreignKey, + 'type': fileEnum.type, + 'path': fileEnum.path, + 'files': await MultipartFile.fromFile(file.path, filename: fileName), + }, + ); + } + + /// 多文件上传 + static Future> uploadFiles( + List imagePaths, + UploadFileType fileEnum, + String hiddenId, + ) async { + List files = []; + for (int i = 0; i < imagePaths.length; i++) { + final file = File(imagePaths[i]); + if (!await file.exists()) { + throw ApiException('file_not_found', '图片不存在:${imagePaths[i]}'); + } + final fileName = file.path.split(Platform.pathSeparator).last; + files.add(await MultipartFile.fromFile(file.path, filename: fileName)); + } + + return HttpManager().uploadImages( + baseUrl: ApiService.basePath, + path: '/basicInfo/imgFiles/batchSave', + fromData: { + 'foreignKey': hiddenId, + 'type': fileEnum.type, + 'path': fileEnum.path, + 'files': files, + }, + ); + } + + /// 删除已上传图片 + static Future> deleteImage(String path) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/imgFiles/delete?filePath=$path', + method: Method.delete, + data: { + }, + ); + } + /// 删除多图 + static Future> deleteImages(List ids) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/imgFiles/ids?ids=${ids.join(",")}', + method: Method.delete, + data: { + }, + ); + } + + + /// 获取图片路径 + static Future> getImagePath(String id, UploadFileType fileEnum) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/imgFiles/listAll', + method: Method.get, + data: { + "eqForeignKey": id, + "eqType": fileEnum.type, + }, + ); + } + /// 获取图片路径 + static Future> getImagePathWithType(String eqForeignKey,String inForeignKey, UploadFileType typeEnum) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/imgFiles/listAll', + method: Method.get, + data: { + /// 外键id + "eqForeignKey": eqForeignKey, + /// 外键ids,多个逗号分割 + "inForeignKey": inForeignKey, + "eqType": typeEnum.type, + }, + ); + } +} \ No newline at end of file diff --git a/lib/http/modules/hidden_danger_api.dart b/lib/http/modules/hidden_danger_api.dart new file mode 100644 index 0000000..080aece --- /dev/null +++ b/lib/http/modules/hidden_danger_api.dart @@ -0,0 +1,492 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; + +class HiddenDangerApi { + /// 获取隐患部位 + static Future> getHiddenDangerAreas() { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenRegion/listAll', + method: Method.post, + data: { + + }, + ); + } + + /// 获取隐患级别列表 + static Future> getHazardLevel() { + return HttpManager().request( + ApiService.basePath, + '/config/dict-trees/list/by/dictValues?appKey=${ApiService.appKey}&dictValue=hiddenLevel', + method: Method.get, + data: { + + }, + ); + } + + /// 获取隐患类型列表 + static Future> getHiddenDangerType() { + return HttpManager().request( + ApiService.basePath, + '/config/dict-trees/list/by/dictValues?appKey=${ApiService.appKey}&dictValue=hiddenType', + method: Method.get, + data: { + + }, + ); + } + + /// 获取部门 + static Future> getHiddenTreatmentListTree() { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/department/listTree', + method: Method.post, + data: { + // "eqCorpinfoId": "1984137837376081921", + }, + ); + } + + /// 获取人员 + static Future> getListTreePersonList(String id) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/user/listAll?departmentId=$id', + method: Method.get, + data: { + // "token": SessionService.instance.token, + }, + ); + } + + /// 获取隐患确认人 + static Future> getHazardPersonlist() { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenConfirmUser/listAll', + method: Method.post, + data: { + // "token": SessionService.instance.token, + }, + ); + } + + /// 新增隐患 + static Future> addRiskListCheckApp(Map data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/save', + method: Method.post, + data: { + ...data, + // "token": SessionService.instance.token, + }, + ); + } + + /// 获取确认列表 + static Future> getConfirmationList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/confirmList', + method: Method.post, + data: { + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取忽略列表 + static Future> getIgnoreList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/ignoreList', + method: Method.post, + data: { + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取整改列表 + static Future> getRectificationList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/rectifyList', + method: Method.post, + data: { + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取特殊处置列表 + static Future> getSpecialHandlingList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/extensionHiddenList', + method: Method.post, + data: { + "eqType": '1', + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取延期审核列表 + static Future> getDelayReviewList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/extensionHiddenList', + method: Method.post, + data: { + "eqType": '2', + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取隐患验收列表 + static Future> getHiddenDangerAcceptanceList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/checkList', + method: Method.post, + data: { + "pageIndex": page, + "hiddenDesc": search, + + }, + ); + } + + /// 获取隐患详情 + static Future> getDangerDetail(String id) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/$id', + method: Method.get, + data: { + + }, + ); + } + + /// 获取图片 + static Future> getImagePath(String id,String type) { + return HttpManager().request( + ApiService.basePath, + '/basicInfo/imgFiles/listAll', + method: Method.get, + data: { + "eqForeignKey": id, + "eqType": type, + }, + ); + } + + + /// 一般隐患管理列表 + static Future> getHazardManagementList(int page,String search) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/generalHiddenListByCorp', + method: Method.post, + data: { + "pageIndex": page, + "corpName": search, + + }, + ); + } + + /// 监管端:一般隐患列表 + static Future> getGeneralHazardList(int page,String search,searchData, String corpId) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/generalHiddenList', + method: Method.post, + data: { + "pageIndex": page, + "hiddenDesc": search, + "corpId": corpId, + + "hiddenFindTime": searchData['beginTIme'], + "hiddenFindTimeLe": searchData['endTime'], + // "hiddenFindTime": searchData['buMenId'], + "hiddenFindDept": searchData['buMenName'], + // "hiddenFindTime": searchData['findUserId'], + "creatorName": searchData['findUserName'], + // "confirmId": searchData['findUserName'], + "confirmUserName": searchData['trueUserName'], + "hiddenType": searchData['type'], + + }, + ); + } + + /// 删除隐患 + static Future> deleteHiddenDangers(String id) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/$id', + method: Method.delete, + data: { + + }, + ); + } + + /// 隐患确认 + static Future> setHazardConfirmation(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/confirm', + method: Method.put, + data: { + ...data, + }, + ); + } + + /// 申请延期 + static Future> setRequestExtension(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenExamine/requestAnExtension', + method: Method.post, + data: { + ...data, + }, + ); + } + + /// 申请特殊处置隐患 + static Future> setReasonInability(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenExamine/requestAnSpecial', + method: Method.post, + data: { + ...data, + }, + ); + } + + /// 隐患整改 + static Future> setHiddenDangerRectification(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/rectify', + method: Method.put, + data: { + ...data, + }, + ); + } + + + + /// 审批特殊处置审核 + static Future> setApprovalSpecialDisposal(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenExamine/reviewSpecial', + method: Method.post, + data: { + ...data, + }, + ); + } + + /// 审批延期申请 + static Future> setApprovalExtensionApplication(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hiddenExamine/reviewExtension', + method: Method.post, + data: { + ...data, + }, + ); + } + + /// 隐患验收 + static Future> setHiddenDangerAcceptance(data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/check', + method: Method.put, + data: { + ...data, + }, + ); + } + /// 根据隐患外键ID查询隐患列表 + static Future> getHiddenDangerListByforeignKeyId(Map data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/queryHiddenListByForeign', + method: Method.post, + data: { + ...data + }, + ); + } + + /// APP隐患排查-首页列表(待排查,已排查) + static Future> getHazardInvestigationList(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/listManager/checkAppPage', + method: Method.post, + data: { + ...data, + }, + ); + } + + + /// 详情 + static Future> getRiskPointsList(String id) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/listManager/info/$id', + method: Method.post, + data: { + + }, + ); + } + + /// 新增事故下的风险点提交 + static Future> submitInvestigationItemsYinHuan(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/checkRecordAccidentItem/accidentSave', + method: Method.post, + data: { + ...data, + }, + ); + } + + + /// APP检查清单总提交 + static Future> customCheckRecordFinishYinHuan(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/checkRecord/save', + method: Method.post, + data: { + ...data, + }, + ); + } + + ///App股份端 -主界面所有企业统计 + static Future> getStockTerminalMainInterfaceList(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/checkRecord/corpCheckRecordStatistics', + method: Method.post, + data: { + ...data, + }, + ); + } + + + ///App股份端 -主界面所有企业统计 + static Future> getStockSecondaryPageList(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/listManager/list', + method: Method.post, + data: { + ...data, + }, + ); + } + + + ///APP清单检查记录-检查记录分页(已检查,超期未检查) + static Future> getCheckRecordList(data) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/checkRecord/list', + method: Method.post, + data: { + ...data, + }, + ); + } + + ///APP清单检查记录-检查记录分页(已检查,超期未检查) + static Future> getCheckRecordDetails(String id) { + return HttpManager().request( + ApiService.basePath, + '/risk/app/checkRecord/info/$id', + method: Method.post, + data: { + + }, + ); + } + + + /// 详情ByHiddeNiD + static Future> getDetailByHiddeNiD(String id) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/hidden/$id', + method: Method.get, + data: { + + }, + ); + } + /// 隐患修改 + static Future> dangerChangeEdit(Map data) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/edit', + method: Method.put, + data: { + + }, + ); + } + + + /// ai识别图片 + static Future> aiRecognitionImages(String imagePath) { + return HttpManager().request( + '${ApiService.basePath}/hidden', + '/hidden/aiHidden', + method: Method.post, + data: { + // ...data, + "hiddenUrl": imagePath, + }, + ); + } + + + +} \ No newline at end of file diff --git a/lib/http/modules/safety_check_api.dart b/lib/http/modules/safety_check_api.dart new file mode 100644 index 0000000..d28cf58 --- /dev/null +++ b/lib/http/modules/safety_check_api.dart @@ -0,0 +1,138 @@ +import 'package:dio/dio.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; + +// 安全环保检查 +class SafetyCheckApi { + /// 安全环保检查发起 + static Future> safeCheckAdd(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/initiate', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查列表 + static Future> safeCheckList(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/page', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查流程图 + static Future> safeCheckFlow(String id) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/flowchart/$id', + method: Method.post, + data: { + 'id':id + }, + ); + } + + /// 安全环保检查人核实 + static Future> safeCheckVerify(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/verify', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查被检查人确认 + static Future> safeCheckConfirm(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/confirm', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查隐患指派 + static Future> safeCheckHiddenAssign(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/assign', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查验收 + static Future> safeCheckAcceptance(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/accept', + method: Method.post, + data: {...data}, + ); + } + + /// 查询安全环保检查详情 + static Future> safeCheckDetail(String inspectionId) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/detail', + method: Method.post, + data: {"inspectionId": inspectionId}, + ); + } + + /// 删除安全环保检查 + static Future> safeCheckDelete(String inspectionId) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/remove/$inspectionId', + method: Method.post, + data: {}, + ); + } + + /// 安全环保检查申辩列表 + static Future> safeCheckAppealList(String id) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/defense/$id', + method: Method.post, + data: {}, + ); + } + + /// 安全环保检查申辩审核 + static Future> safeCheckAppealAudit(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/defenseReview', + method: Method.post, + data: {...data}, + ); + } + + /// 安全环保检查编辑修改 + static Future> safeCheckEdit(Map data) { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/edit', + method: Method.post, + data: {...data}, + ); + } + + /// 获取检查数量 + static Future> safeCheckCount() { + return HttpManager().request( + '${ApiService.basePath}/inspection', + '/safetyEnvironmentalInspection/getCount', + method: Method.post, + data: {}, + ); + } +} diff --git a/lib/http/modules/safety_commitment_api.dart b/lib/http/modules/safety_commitment_api.dart new file mode 100644 index 0000000..547a712 --- /dev/null +++ b/lib/http/modules/safety_commitment_api.dart @@ -0,0 +1,71 @@ +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 SafetyCommitmentApi { + + /// 获取需要签字的承诺书 + static Future> getNeedSafetyCommitment() { + return HttpManager().request( + '${ApiService.basePath}/promise', + '/busPromisePeople/getSignPromisePeople', + method: Method.get, + data: { + // ...data, + }, + ); + } + + /// 承诺书详情 + static Future> getSafetyCommitmentDetail(String id) { + return HttpManager().request( + '${ApiService.basePath}/promise', + '/busPromisePeople/$id', + method: Method.get, + data: { + // ...data, + }, + ); + } + + /// 承诺书签字 + static Future> upPromiseBookmarkApply( data) { + return HttpManager().request( + '${ApiService.basePath}/promise', + '/busPromisePeople/signPromisePeople', + method: Method.put, + data: { + ...data, + }, + ); + } + + /// 我的承诺列表 + static Future> getMySecurityPromiseList( data) { + return HttpManager().request( + '${ApiService.basePath}/promise', + '/busPromise/myListByCommitment', + method: Method.post, + data: { + ...data, + }, + ); + } + + /// 我的承诺列表 + static Future> getSignRecordPageData( String id) { + return HttpManager().request( + '${ApiService.basePath}/promise', + '/busPromise/$id', + method: Method.get, + data: { + // ...data, + }, + ); + } + + + + +} \ No newline at end of file diff --git a/lib/http/modules/special_work_api.dart b/lib/http/modules/special_work_api.dart new file mode 100644 index 0000000..e7b2b4f --- /dev/null +++ b/lib/http/modules/special_work_api.dart @@ -0,0 +1,91 @@ +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/http/HttpManager.dart'; +class SpecialWorkApi { + /// 特殊作业初始化 + static Future> specialWorkInit(String gateway) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskFlow/getWorkInit', + method: Method.post, + data: { + 'workType': gateway, + }, + ); + } + + /// 特殊作业新增 + static Future> specialWorkSave(Map data) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskLog/save', + method: Method.post, + data: {...data}, + ); + } + + /// 初始化流程 + static Future> specialWorkFlowInit(Map data) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskFlow/getFlowInit', + method: Method.post, + data: {...data}, + ); + } + /// 代办列表 + static Future> specialWorkTaskLogList(Map data) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskLog/list', + method: Method.post, + data: {...data}, + ); + } + /// 获取详情 + static Future> specialWorkTaskLogDetail(String id) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskLog/$id', + method: Method.get, + data: {}, + ); + } + /// 获取安全措施列表 + static Future> specialWorkMeasureList(String eqWorkType) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/measures/listAll', + method: Method.post, + data: {'eqWorkType': eqWorkType}, + ); + } + /// 特殊作业下一步taskLog/nextStep + static Future> specialWorkNextStep(Map data) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskLog/nextStep', + method: Method.post, + data: {...data}, + ); + } + /// 查看流程图 + static Future> specialWorkFlowList(String workId) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/taskLog/listAll/$workId', + method: Method.get, + data: {}, + ); + } + /// 获取作业安全措施详情 + static Future> specialWorkMeasureDetail(String workId) { + return HttpManager().request( + ApiService.basePath, + '/eightwork/measuresLogs/listAll/$workId', + method: Method.get, + data: {}, + ); + } + + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..701a87b --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,336 @@ +// main.dart +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/main_tab.dart'; +import 'package:qhd_prevention/pages/user/login_page.dart'; +import 'package:qhd_prevention/services/StorageService.dart'; +import 'package:qhd_prevention/services/auth_service.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'http/HttpManager.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter/services.dart'; // for TextInput.hide +import 'package:flutter_baidu_mapapi_base/flutter_baidu_mapapi_base.dart' + show BMFMapSDK, BMF_COORD_TYPE; +import 'package:flutter_baidu_mapapi_map/flutter_baidu_mapapi_map.dart'; +import 'package:qhd_prevention/common/route_observer.dart'; +import 'pages/mine/mine_set_pwd_page.dart'; + +// 全局导航键 +final GlobalKey navigatorKey = GlobalKey(); +bool _isLoggingOut = false; +// 全局消息控制器 +class GlobalMessage { + static void showError(String message) { + final context = navigatorKey.currentContext; + if (context != null) { + ToastUtil.showError(context, message); + } + } +} + +/// 全局 helper:在弹窗前取消焦点并尽量隐藏键盘,避免弹窗后键盘自动弹起 +Future showDialogAfterUnfocus( + BuildContext context, + Widget dialog, + ) async { + // 取消焦点并尝试隐藏键盘 + FocusScope.of(context).unfocus(); + try { + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + } catch (_) {} + // 给系统一点时间,避免竞态(100-200ms 足够) + await Future.delayed(const Duration(milliseconds: 150)); + return showDialog(context: context, builder: (_) => dialog); +} + +/// 同理:在展示底部模态前确保键盘隐藏 +Future showModalBottomSheetAfterUnfocus({ + required BuildContext context, + required WidgetBuilder builder, + bool isScrollControlled = false, +}) async { + FocusScope.of(context).unfocus(); + try { + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 150)); + return showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + builder: builder, + ); +} + +void main( ) async { + WidgetsFlutterBinding.ensureInitialized(); + StorageService.instance.init(); + // 1) 同意 SDK 隐私(百度 SDK 要求) + BMFMapSDK.setAgreePrivacy(true); + + // 2) 初始化鉴权 / 坐标类型 + if (Platform.isIOS) { + // iOS 可以通过接口直接设置 AK + BMFMapSDK.setApiKeyAndCoordType('g3lZyqt0KkFnZGUsjIVO7U6lTCfpjSCt', BMF_COORD_TYPE.BD09LL); + } else if (Platform.isAndroid) { + // Android 插件示例:Android 的 AK 通过 Manifest 配置 + BMFMapSDK.setApiKeyAndCoordType('43G1sKuHV6oRTrdR9VTIGPF9soej7V5a', BMF_COORD_TYPE.BD09LL); + await BMFAndroidVersion.initAndroidVersion(); // 可选,插件示例中有 + } + + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + // 初始化 EasyLoading + EasyLoading.instance + ..displayDuration = const Duration(seconds: 20) + ..indicatorType = EasyLoadingIndicatorType.ring + ..loadingStyle = EasyLoadingStyle.custom + ..indicatorSize = 36.0 + ..radius = 0 + ..progressColor = Colors.blue + ..backgroundColor = Colors.grey.shade300 + ..indicatorColor = Colors.blue + ..textColor = Colors.black + ..userInteractions = false + ..dismissOnTap = false; + + await initializeDateFormatting('zh_CN', null); + + // 初始化HTTP管理器未授权回调 + // 全局 flag,确保不会重复弹窗 + bool _isLoggingOut = false; + bool _isDialogShowing = false; + + HttpManager.onUnauthorized = () async { + return; + final navState = navigatorKey.currentState; + if (navState == null) return; + + // 如果已经在登录页就不弹了 + final currentRouteName = ModalRoute.of(navState.context)?.settings.name; + if (currentRouteName == '/login') return; + + if (_isLoggingOut) return; + _isLoggingOut = true; + + try { + // 清理登录信息(提前做或在用户确认后做,根据你业务决定) + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isLoggedIn', false); + await prefs.remove('token'); + + // 获取最安全的 context(overlay context 优先),保证能在任意 route 上显示 dialog + final dialogContext = navState.overlay?.context ?? navState.context; + + if (!_isDialogShowing) { + _isDialogShowing = true; + // 跳回登录页 + navState.pushNamedAndRemoveUntil('/login', (route) => false); + // 弹出不可通过点击外部关闭的对话框 + await showDialog( + context: dialogContext, + barrierDismissible: false, // + builder: (context) { + return PopScope( + canPop: false, + // 禁止返回键关闭(Android) + child: AlertDialog( + title: const Text('提示'), + content: const Text('您的账号已在其他设备登录,已自动下线,请使用单一设备进行学习。'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // 关闭 dialog + }, + child: const Text('确定', style: TextStyle(color: Colors.blue),), + ), + ], + ), + ); + }, + ); + _isDialogShowing = false; + } + + + } finally { + _isLoggingOut = false; + _isDialogShowing = false; + } + }; + // 自动登录逻辑 + final prefs = await SharedPreferences.getInstance(); + // final savedLogin = prefs.getBool('isLoggedIn') ?? false; + final savedLogin = false; + + // bool isLoggedIn = false; + Map data={}; + + if (savedLogin) { + // 如果本地标记已登录,进一步验证 token 是否有效 + try { + // isLoggedIn = await AuthService.isLoggedIn(); + data =await AuthService.isLoggedIn(); + // if (data.isEmpty||data['result']!= 'success') { + // isLoggedIn =false; + // }else{ + // isLoggedIn = true; + // } + } catch (e) { + data={}; + // isLoggedIn = false; + } + } + + // runApp(MyApp(isLoggedIn: isLoggedIn)); + runApp(MyApp(data: data)); +} + +/// MyApp:恢复为 Stateless(无需监听 viewInsets) +class MyApp extends StatelessWidget { + // final bool isLoggedIn; + final Map data; + const MyApp({super.key, required this.data}); + + Widget getHomePage() { + + /** + if (data.isNotEmpty&&data['result'] == 'success') { + if(data['WEAK_PASSWORD']=='1'){ + return const MineSetPwdPage("3"); + + }else if(data['LONG_TERM_PASSWORD_NOT_CHANGED']=='1'){ + return const MineSetPwdPage("4"); + }else{ + return const MainPage(); + } + + }else{ + return const LoginPage(); + } + */ + return const LoginPage(); + // return MainPage(isChooseFirm: false); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: '', + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + // 如果使用了其他本地化包,请添加对应的 delegate + ], + supportedLocales: const [ + Locale('zh', 'CN'), // 中文 + Locale('en', 'US'), // 英文(备用) + ], + locale: const Locale('zh', 'CN'), + // 强制使用中文 + navigatorKey: navigatorKey, + // 在路由变化时统一取消焦点(防止 push/pop 时焦点回到 TextField) + navigatorObservers: [KeyboardUnfocusNavigatorObserver(), routeObserver], + builder: (context, child) { + // 使用 EasyLoading.init,同时在其内部把 textScaleFactor 固定为 1.0 + return EasyLoading.init( + builder: (context, widget) { + // 拿到当前 MediaQuery 并创建一个 textScaleFactor = 1.0 的副本 + final mq = MediaQuery.maybeOf(context) ?? MediaQueryData.fromWindow(WidgetsBinding.instance.window); + final fixed = mq.copyWith(textScaleFactor: 1.0); + + return MediaQuery( + data: fixed, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + // 全局点击空白处取消焦点(隐藏键盘) + try { + FocusManager.instance.primaryFocus?.unfocus(); + } catch (e) { + debugPrint('NavigatorObserver unfocus error: $e'); + } + }, + child: widget, + ), + ); + }, + )(context, child); + }, + theme: ThemeData( + textTheme: const TextTheme( + bodyMedium: TextStyle(fontSize: 13), // 默认字体大小 + ), + dividerTheme: const DividerThemeData( + color: Colors.black12, + thickness: .5, + indent: 0, + endIndent: 0, + ), + primarySwatch: Colors.blue, + scaffoldBackgroundColor: const Color(0xFFF1F1F1), + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + inputDecorationTheme: const InputDecorationTheme( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 8), + ), + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: Colors.blue, + ), + ), + home:getHomePage(), + // isLoggedIn ? const MainPage() : const LoginPage(), + debugShowCheckedModeBanner: false, + routes: {'/login': (_) => const LoginPage()}, + ); + } +} + +/// NavigatorObserver:在 push/pop/remove/replace 等路由变化时统一取消焦点 +class KeyboardUnfocusNavigatorObserver extends NavigatorObserver { + void _unfocus() { + try { + FocusManager.instance.primaryFocus?.unfocus(); + } catch (e) { + debugPrint('NavigatorObserver unfocus error: $e'); + } + } + + @override + void didPush(Route route, Route? previousRoute) { + _unfocus(); + super.didPush(route, previousRoute); + } + + @override + void didPop(Route route, Route? previousRoute) { + _unfocus(); + super.didPop(route, previousRoute); + } + + @override + void didRemove(Route route, Route? previousRoute) { + _unfocus(); + super.didRemove(route, previousRoute); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + _unfocus(); + super.didReplace(newRoute: newRoute, oldRoute: oldRoute); + } +} diff --git a/lib/pages/home/ai_page.dart b/lib/pages/home/ai_page.dart new file mode 100644 index 0000000..383e41a --- /dev/null +++ b/lib/pages/home/ai_page.dart @@ -0,0 +1,545 @@ +import 'dart:convert'; +import 'dart:io'; + +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/file_api.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +// 单个隐患数据模型 +class HiddenItem { + String? rectificationSuggestions; + String? hiddenDescr; + String? legalBasis; + bool isSelect=false; + + HiddenItem({ + this.rectificationSuggestions, + this.hiddenDescr, + this.legalBasis, + + }); + + factory HiddenItem.fromJson(Map json) { + return HiddenItem( + rectificationSuggestions: json['rectificationSuggestions'] as String?, + hiddenDescr: json['hiddenDescr'] as String?, + legalBasis: json['legalBasis'] as String?, + ); + } + + Map toJson() { + return { + 'rectificationSuggestions': rectificationSuggestions, + 'hiddenDescr': hiddenDescr, + 'legalBasis': legalBasis, + }; + } +} + + +class AiPage extends StatefulWidget { + const AiPage(this.imagePath, {super.key}); + + final String imagePath; + + @override + State createState() => _AiPageState(); +} + +class _AiPageState extends State with SingleTickerProviderStateMixin { + Image? _selectedImage; + bool _isScanning = false; + bool _showResult = false; + late AnimationController _animationController; + late Animation _animation; + + List hiddenItems = []; + + // List serialNumbers = [ + // 'SN-001-2024', + // 'SN-002-2024', + // 'SN-003-2024', + // 'SN-004-2024', + // 'SN-005-2024', + // 'SN-006-2024', + // 'SN-007-2024', + // ]; + + // Map selectedItems = {}; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: Duration(seconds: 2), // 单次扫描时间改为2秒 + vsync: this, + ); + + // 使用往复动画,让扫描线来回移动 + _animation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + + // // 初始化选择状态 + // for (var serial in serialNumbers) { + // selectedItems[serial] = false; + // } + + _pickImage(); + _getAiRecognize(); + } + + + /// 手动拍照并上传 + Future _getAiRecognize() async { + + try { + + final raw = await FileApi.uploadFile( widget.imagePath, UploadFileType.aiRecognitionImages,''); + if (raw['success'] ) { + // String imagePath= ApiService.baseImgPath+raw['data']['filePath']; + String imagePath= 'https://jpfz.qhdsafety.com/gbsFileTest/20251201171655.jpg'; + final res = await HiddenDangerApi.aiRecognitionImages(imagePath); + + if (res['success'] ) { + List data=res['data']['aiHiddens']??[]; + if(data.isNotEmpty){ + + for(int i=0;i; + hiddenItems.add(HiddenItem.fromJson(itemJson)); + + } catch (e) { + print('解析失败: $e, 原始数据: $item'); + } + } else if (data[i] is Map) { + hiddenItems.add(HiddenItem.fromJson(data[i])); + } + } + + _animationController.stop(); + setState(() { + _isScanning = false; + _showResult = true; + }); + + }else { + ToastUtil.showError(context, '未获取到隐患,请重新拍照'); + } + + } else { + ToastUtil.showError(context, '识别失败,请重试'); + } + + }else{ + // _showMessage('反馈提交失败'); + ToastUtil.showError(context, '识别失败,请重试'); + } + + } catch (e, st) { + debugPrint('[FaceRecognition] manual capture error: $e\n$st'); + ToastUtil.showError(context, '识别失败,请重试'); + } + + } + + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + // 这里使用 image_picker 包来选择图片 + // final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery); + + // 为了演示,我们使用一个网络图片 + setState(() { + _selectedImage = Image.file( + File(widget.imagePath),//'https://picsum.photos/400/600', + fit: BoxFit.cover, + ); + _isScanning = true; + }); + + // 重置动画到开始位置(顶部) + _animationController.value = 0.0; + + // 开始往复扫描动画 + _animationController.repeat(reverse: true); + + // 5秒后结束扫描 + // Future.delayed(Duration(seconds: 5), () { + // _animationController.stop(); + // setState(() { + // _isScanning = false; + // _showResult = true; + // }); + // }); + } + + Widget _buildImagePickerBox() { + return GestureDetector( + onTap: _pickImage, + child: Container( + width: 200, + height: 150, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 2), + borderRadius: BorderRadius.circular(12), + ), + child: + _selectedImage == null + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.photo_library, size: 40, color: Colors.grey), + SizedBox(height: 8), + Text('选择图片', style: TextStyle(color: Colors.grey)), + ], + ) + : ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _selectedImage, + ), + ), + ); + } + + Widget _buildScanLine() { + + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + + // 计算扫描线位置,从顶部开始,确保不超出屏幕底部 + double screenHeight = (MediaQuery.of(context).size.height)-100; + double scanLineHeight = 4.0; // 扫描线高度 + + // 计算扫描线顶部位置,确保扫描线底部不超出屏幕 + double maxTopPosition = screenHeight - scanLineHeight; + double scanTopPosition = screenHeight * _animation.value; + + // 限制扫描线位置在屏幕范围内 + scanTopPosition = scanTopPosition.clamp(0.0, maxTopPosition); + + return Positioned( + top: scanTopPosition, + child: Container( + width: MediaQuery.of(context).size.width, + height: 3, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.blue, + Colors.blue, + Colors.blue, + Colors.transparent, + ], + stops: [0.0, 0.2, 0.5, 0.8, 1.0], + ), + boxShadow: [ + BoxShadow( + color: Colors.blue.withOpacity(0.5), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildScanOverlay() { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.black.withOpacity(0.7), + Colors.black.withOpacity(0.3), + Colors.transparent, + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.7), + ], + stops: [0.0, 0.2, 0.5, 0.8, 1.0], + transform: GradientRotation(_animation.value * 3.14159), // 旋转渐变方向 + ), + ), + ); + }, + ); + } + + Widget _buildResultPanel() { + return Positioned( + top: 20, + left: 20, + child: AnimatedContainer( + duration: Duration(milliseconds: 500), + width: _showResult ? 80 : 200, + height: _showResult ? 60 : 150, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey), + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 8, + offset: Offset(2, 2), + ), + ], + ), + child: + _selectedImage != null + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _selectedImage, + ) + : SizedBox(), + ), + ); + } + + Widget _buildSerialNumberList() { + return AnimatedOpacity( + opacity: _showResult ? 1.0 : 0.0, + duration: Duration(milliseconds: 500), + child: Container( + margin: EdgeInsets.only(top: 40, left: 20, right: 20, bottom: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '检测到的隐患:', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Expanded( + child: Container( + + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.builder( + itemCount: hiddenItems.length, + itemBuilder: (context, index) { + final serial = hiddenItems[index].hiddenDescr; + return Container( + decoration: BoxDecoration( + border: Border( + bottom: + index < hiddenItems.length - 1 + ? BorderSide(color: Colors.grey.shade300) + : BorderSide.none, + ), + ), + child: CheckboxListTile( + title: Text(serial??'', style: TextStyle(fontSize: 14)), + value: hiddenItems[index].isSelect , + onChanged: (bool? value) { + setState(() { + hiddenItems[index].isSelect=value ?? false; + // selectedItems[serial??''] = value ?? false; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ), + ); + }, + ), + ), + ), + + SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: ElevatedButton( + onPressed: () { + // 取消操作 + _resetPage(); + + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey.shade400, + foregroundColor: Colors.white, + minimumSize: Size(0, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text('合并', style: TextStyle(fontSize: 16)), + ), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: ElevatedButton( + onPressed: () { + // 确认操作 + _showConfirmationDialog(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: Size(0, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text('处理', style: TextStyle(fontSize: 16)), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _showConfirmationDialog() { + // final selectedSerials = + // selectedItems.entries + // .where((entry) => entry.value) + // .map((entry) => entry.key) + // .toList(); + // + // showDialog( + // context: context, + // builder: + // (context) => AlertDialog( + // title: Text('确认选择'), + // content: Column( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text('已选择 ${selectedSerials.length} 个序列号:'), + // SizedBox(height: 10), + // if (selectedSerials.isNotEmpty) + // ...selectedSerials + // .map((serial) => Text('• $serial')) + // .toList(), + // ], + // ), + // actions: [ + // TextButton( + // onPressed: () => Navigator.of(context).pop(), + // child: Text('确定'), + // ), + // ], + // ), + // ); + } + + void _resetPage() { + // setState(() { + // _selectedImage = null; + // _isScanning = false; + // _showResult = false; + // _animationController.reset(); + // + // // // 重置选择状态 + // // for (var key in selectedItems.keys) { + // // selectedItems[key] = false; + // // } + // }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: 'Ai识别'), + body: Stack( + children: [ + // 主内容 + Column( + children: [ + SizedBox(height: 50), + // if (!_showResult) + // Center(child: _buildImagePickerBox()), + if (_showResult) Expanded(child: _buildSerialNumberList()), + ], + ), + + // 全屏扫描效果 + if (_isScanning && _selectedImage != null) + Container( + color: Colors.black, + child: Stack( + children: [ + // 全屏图片 + SizedBox.expand(child: _selectedImage), + + // 扫描遮罩效果 + _buildScanOverlay(), + + // 扫描线 + _buildScanLine(), + + // 扫描提示 + Positioned( + top: 50, + left: 0, + right: 0, + child: Center( + child: Column( + children: [ + Text( + '扫描中...', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10), + Container( + width: 30, + height: 30, + child: CircularProgressIndicator( + color: Colors.blue, + strokeWidth: 3, + ), + ), + ], + ), + ), + ), + ], + ), + ), + + // 结果面板 + if (_showResult) _buildResultPanel(), + ], + ), + ); + } +} diff --git a/lib/pages/home/certificate/certificate_detail_page.dart b/lib/pages/home/certificate/certificate_detail_page.dart new file mode 100644 index 0000000..2fae8c3 --- /dev/null +++ b/lib/pages/home/certificate/certificate_detail_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.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'; +import 'package:qhd_prevention/customWidget/photo_picker_row.dart'; +import 'package:qhd_prevention/pages/home/certificate/certificate_list_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +class CertificateDetailPage extends StatefulWidget { + const CertificateDetailPage({super.key, required this.model}); + final CertifitcateEditMode model; + + @override + State createState() => _CertificateDetailPageState(); +} + +class _CertificateDetailPageState extends State { + final pd = {}; + + Future _saveSuccess() async { + + + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: widget.model == CertifitcateEditMode.edit ?'证书信息添加': '查看信息', isBack: true), + body: SafeArea( + child: ItemListWidget.itemContainer( + horizontal: 5, + ListView( + children: [ + RepairedPhotoSection( + title: '证书正面图片', + inlineSingle: true, + isRequired: true, + horizontalPadding: 12, + inlineImageWidth: 60, + onChanged: (files) { + /* files 长度 <= 1 */ + }, + onAiIdentify: () { + /* ... */ + }, + ), + const Divider(), + RepairedPhotoSection( + title: '证书反面图片', + inlineSingle: true, + isRequired: true, + horizontalPadding: 12, + inlineImageWidth: 60, + onChanged: (files) { + /* files 长度 <= 1 */ + }, + onAiIdentify: () { + /* ... */ + }, + ), + + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '证书作业类型:', + isEditable: true, + text: '请选择', + isRequired: true, + onTap: () { + /* ... */ + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '证书名称:', + isRequired: true, + hintText: '请输入证书名称', + isEditable: true, + onChanged: (value) { + pd['address'] = value; + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '证书编号:', + isRequired: true, + hintText: '请输入证书编号', + isEditable: true, + onChanged: (value) { + pd['address'] = value; + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '有效期开始时间:', + isEditable: true, + text: '请选择', + isRequired: true, + onTap: () { + /* ... */ + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '有效期结束时间:', + isEditable: true, + text: '请选择', + isRequired: true, + onTap: () { + /* ... */ + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '复审时间:', + isEditable: true, + text: '请选择', + isRequired: true, + onTap: () { + /* ... */ + }, + ), + const Divider(), + const SizedBox(height: 20), + CustomButton(text: '保存', backgroundColor: Colors.blue, onPressed: () { + + },) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/certificate/certificate_list_page.dart b/lib/pages/home/certificate/certificate_list_page.dart new file mode 100644 index 0000000..df5c113 --- /dev/null +++ b/lib/pages/home/certificate/certificate_list_page.dart @@ -0,0 +1,248 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/item_list_widget.dart'; +import 'package:qhd_prevention/customWidget/toast_util.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/tools/tools.dart'; +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'; + +/// 六种页面模式:发起、检查人确认、被检查人签字及申辩、隐患指派及验收、申辩记录、检查验收 +enum CertifitcateEditMode { delete, detail, edit } + +class CertificateListPage extends StatefulWidget { + final String flow; + + const CertificateListPage({Key? key, required this.flow}) : super(key: key); + + @override + _CertificateListPageState createState() => _CertificateListPageState(); +} + +class _CertificateListPageState extends State { + // 列表数据与分页 + List list = []; + int currentPage = 1; + int rows = 10; + int totalPage = 1; + bool isLoading = false; + + Map flowData = {}; + final GlobalKey _scaffoldKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _fetchData(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.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 = false; + list = [ + {'name': 1}, + ]; + }); + + try { + // final data = { + // 'status': _computeStatusParam(), + // 'pageSize': rows, + // 'pageIndex': currentPage, + // 'keywords': _searchController.text.trim(), + // }; + // final response = await SafetyCheckApi.safeCheckList(data); + // + // setState(() { + // final newData = response['data'] as List? ?? []; + // if (currentPage == 1) { + // list = newData; + // } else { + // list.addAll(newData); + // } + // totalPage = response['totalPage'] ?? 1; + // isLoading = false; + // }); + } catch (e) { + print('Error fetching data: $e'); + setState(() => isLoading = false); + ToastUtil.showNormal(context, '获取列表失败'); + } + } + + void _search() { + currentPage = 1; + list.clear(); + _fetchData(); + } + + /// 打开流程图侧栏 + Future _openFlowDrawer(String id) async { + try { + final response = await SafetyCheckApi.safeCheckFlow(id); + final List? newFlow = response['data']?['flowCOList']; + printLongString(jsonEncode(response['data'])); + if (newFlow == null || newFlow.isEmpty) { + ToastUtil.showNormal(context, '暂无流程图数据'); + return; + } + + setState(() { + flowData = response['data'] ?? {}; + }); + Future.microtask(() { + _scaffoldKey.currentState?.openEndDrawer(); + }); + } catch (e) { + print('Error fetching flow data: $e'); + ToastUtil.showNormal(context, '获取流程图失败: $e'); + } + } + + /// 跳转到详情 + Future _goToDetail( + Map item, + CertifitcateEditMode type, + ) async { + await pushPage(CertificateDetailPage(model: CertifitcateEditMode.detail), context); + // 完成后刷新列表 + currentPage = 1; + _fetchData(); + } + + Widget _buildListItem(Map item) { + return Card( + color: Colors.white, + margin: const EdgeInsets.all(8.0), + child: InkWell( + onTap: () => _goToDetail(item, CertifitcateEditMode.detail), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + Row( + children: [ + Image.asset('assets/images/g_logo.png', width: 80, height: 80), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "单位名称:${item['name'] ?? ''}", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + Text("证书编号: ${item['inspectionOriginatorUserName'] ?? ''}"), + Text("复审时间: ${item['type'] ?? ''}"), + const SizedBox(height: 8), + // 按钮行:如果只有一个按钮,不需要间距;多个按钮均分占满行,间距为 10 + ], + ), + ], + ), + const SizedBox(height: 20,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomButton( + text: '删除', + padding: EdgeInsets.symmetric(horizontal: 20), + height: 35, + onPressed: + () => _goToDetail( + item, + CertifitcateEditMode.delete, + ), + backgroundColor: Colors.red, + ), + Row( + children: [ + CustomButton( + padding: EdgeInsets.symmetric(horizontal: 20), + text: '编辑', + height: 35, + onPressed: + () => _goToDetail( + item, + CertifitcateEditMode.edit, + ), + ), + CustomButton( + text: '查看', + padding: EdgeInsets.symmetric(horizontal: 20), + height: 35, + onPressed: + () => _goToDetail( + item, + CertifitcateEditMode.detail, + ), + ), + ], + ), + ], + ), + ], + ) + + ), + ), + ); + } + + 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: '${widget.flow}'), + body: SafeArea( + child: Column(children: [Expanded(child: _buildListContent())]), + ), + ); + } +} diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart new file mode 100644 index 0000000..4b97e6a --- /dev/null +++ b/lib/pages/home/home_page.dart @@ -0,0 +1,990 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/common/route_aware_state.dart'; +import 'package:qhd_prevention/common/route_service.dart'; +import 'package:qhd_prevention/customWidget/custom_button.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/user/firm_list_page.dart'; +import 'package:qhd_prevention/tools/h_colors.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key, required this.isChooseFirm}) : super(key: key); + final bool isChooseFirm; + + @override + HomePageState createState() => HomePageState(); +} + +class HomePageState extends RouteAwareState + with WidgetsBindingObserver, SingleTickerProviderStateMixin { + final PageController _pageController = PageController(); + final ScrollController _scrollController = ScrollController(); + + int _currentPage = 0; + bool _isMobileSelected = true; // 切换按钮状态 + + void startScan() { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => ScanPage(type: ScanType.Onboarding)), + ); + } + + // 缓存 key + static const String _hiddenCacheKey = 'hidden_roll_cache'; + + // 上面按钮显示状态 + List _buttonVisibility = []; + + // 我的工作子项显示状态 + List _workItemVisibility = []; + List totalList = []; + + // 工作统计数据 + Map workStats = {'total': 36, 'processed': 30, 'pending': 6}; + + // 检查清单数据 + List> checkLists = [ + { + "title": "电工班车间清单", + "type": "隐患排查", + "time": "2025-11-17", + "color": Color(0xFF4CAF50), + }, + { + "title": "工地区消防点检清单", + "type": "消防点检", + "time": "2025-11-17", + "color": Color(0xFF2196F3), + }, + { + "title": "消防专项检查清单", + "type": "安环检查", + "time": "2025-11-17", + "color": Color(0xFFFF9800), + }, + { + "title": "二车间防护网", + "type": "隐患处理", + "time": "2025-11-17", + "color": Color(0xFFF44336), + }, + { + "title": "二车间防护网", + "type": "隐患处理", + "time": "2025-11-17", + "color": Color(0xFFF44336), + }, + { + "title": "二车间防护网", + "type": "隐患处理", + "time": "2025-11-17", + "color": Color(0xFFF44336), + }, + { + "title": "二车间防护网", + "type": "隐患处理", + "time": "2025-11-17", + "color": Color(0xFFF44336), + }, + { + "title": "二车间防护网", + "type": "隐患处理", + "time": "2025-11-17", + "color": Color(0xFFF44336), + }, + ]; + + // 通知相关 + final List _notifications = [ + "系统通知:今晚20:00 将进行系统维护,请提前保存数据。", + "安全提示:施工区请佩戴安全帽并系好安全带。", + "公告:本周五集团例会在多功能厅召开,9:00准时开始。", + "提醒:请尽快完成隐患整改清单中的待办项。", + ]; + int _notifIndex = 0; + late final PageController _notifPageController; + Timer? _notifTimer; + + List> buttonInfos = [ + {"icon": "assets/images/ico1.png", "title": "单位管理", "unreadCount": 0}, + {"icon": "assets/images/ico2.png", "title": "现场监管", "unreadCount": 0}, + {"icon": "assets/images/ico3.png", "title": "危险作业", "unreadCount": 0}, + {"icon": "assets/images/ico4.png", "title": "隐患处理", "unreadCount": 0}, + {"icon": "assets/images/ico5.png", "title": "安环检查", "unreadCount": 0}, + {"icon": "assets/images/ico6.png", "title": "口门门禁", "unreadCount": 0}, + {"icon": "assets/images/ico7.png", "title": "入港培训", "unreadCount": 0}, + ]; + + // 浮动 AppBar(滚动触发) + bool _showFloatingAppBar = false; + static const double _triggerOffset = 30.0; // 滚动触发距离 + static const double _floatingBarHeight = 56.0; + + // 更新模块和按钮显示状态的方法 + void _updateModuleAndButtonVisibility() { + final routeService = RouteService(); + final homeRoutes = + routeService.mainTabs.isNotEmpty + ? routeService.getRoutesForTab(routeService.mainTabs[0]) + : []; + + setState(() { + _buttonVisibility = List.filled(buttonInfos.length, false); + + // 根据路由标题匹配并设置显示状态(目前保留) + for (final route in homeRoutes) { + final routeTitle = route.title ?? ''; + } + }); + } + + /// 隐患播报列表及状态 + List> hiddenList = []; + bool _initialLoadingHidden = true; + bool _firstLoad = false; + + @override + void initState() { + super.initState(); + + // _getNeedSafetyCommitment(); + _buttonVisibility = List.filled(buttonInfos.length, true); + + // 通知滚动 PageController + _notifPageController = PageController(initialPage: 0); + + // 启动定时器:每 3 秒切换一条通知 + _notifTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (!mounted) return; + final next = (_notifIndex + 1) % _notifications.length; + _notifIndex = next; + // animateToPage 可能抛异常(在 dispose 中),所以包在 try/catch + try { + _notifPageController.animateToPage( + next, + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + ); + } catch (_) {} + setState(() {}); + }); + + // 监听主列表滚动以控制浮动 AppBar 显示 + _scrollController.addListener(_onScroll); + + WidgetsBinding.instance.addObserver(this); + Future.delayed(const Duration(seconds: 1), () { + _firstLoad = true; + }); + } + + void _onScroll() { + final offset = + _scrollController.hasClients ? _scrollController.offset : 0.0; + final shouldShow = offset >= _triggerOffset; + if (shouldShow != _showFloatingAppBar) { + setState(() { + _showFloatingAppBar = shouldShow; + }); + } + } + + @override + void dispose() { + _pageController.dispose(); + _notifTimer?.cancel(); + _notifPageController.dispose(); + + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Future onVisible() async { + final current = CurrentTabNotifier.of(context)?.currentIndex ?? 0; + const myIndex = 0; + if (current != myIndex) { + return; + } + if (_firstLoad) {} + } + + void onRouteConfigLoaded() { + if (mounted) { + setState(() { + _updateModuleAndButtonVisibility(); + }); + } + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // App 回到前台时刷新数据 + } + } + + void _onBadgeUpdated() { + if (mounted) { + setState(() {}); + } + } + + /// 更新按钮角标 + void _updateButtonBadges() { + setState(() { + // 可以在这里更新角标数据 + }); + } + + /// 从 SharedPreferences 读取缓存 + Future _loadHiddenCache() async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_hiddenCacheKey); + if (jsonStr != null && jsonStr.isNotEmpty) { + final parsed = jsonDecode(jsonStr) as List; + final list = + parsed.map((e) => Map.from(e as Map)).toList(); + setState(() { + hiddenList = list; + _initialLoadingHidden = false; + }); + } else { + setState(() { + _initialLoadingHidden = true; + }); + } + } catch (e) { + debugPrint('加载 hidden cache 失败: $e'); + setState(() { + _initialLoadingHidden = true; + }); + } + } + + Future _onRefresh() async { + // 刷新数据 + await Future.delayed(const Duration(seconds: 1)); + } + + @override + Widget build(BuildContext context) { + const double notificationHeight = 60.0; // 通知栏高度 + double bannerHeight = 738 / 1125 * screenWidth(context); + const double iconSectionHeight = 220.0; + const double iconOverlapBanner = 90.0; // 图标区覆盖 banner 的高度 + const double iconOverlapNotification = -10.0; // 图标区覆盖通知栏的高度 + + final double stackBottom = + bannerHeight - iconOverlapBanner + iconSectionHeight + 60; + + final double statusBar = MediaQuery.of(context).padding.top; + + return PopScope( + canPop: false, + child: Scaffold( + extendBodyBehindAppBar: true, + body: Stack( + children: [ + RefreshIndicator( + onRefresh: _onRefresh, + child: ListView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(0), + children: [ + SizedBox( + height: stackBottom, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Banner(顶部) + Positioned( + top: 0, + left: 0, + right: 0, + height: bannerHeight, + child: _buildBannerSection(bannerHeight), + ), + + // 通知栏( + Positioned( + left: 10, + right: 10, + top: + (bannerHeight - iconOverlapBanner) + + iconSectionHeight - + iconOverlapNotification, + height: notificationHeight, + child: _buildNotificationBar(notificationHeight - 2), + ), + + // 图标区(覆盖 banner 底部 overlap) + Positioned( + left: 10, + right: 10, + top: bannerHeight - iconOverlapBanner, + height: iconSectionHeight, + child: _buildIconSection(context), + ), + ], + ), + ), + if (widget.isChooseFirm) ...[ + // 工作统计区域 + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: _buildWorkStatsSection(), + ), + + // 检查清单区域 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: _buildCheckListSection(), + ), + + const SizedBox(height: 20), + ] + + ], + ), + ), + + // 浮动 AppBar(平滑出现/隐藏),放在 ListView 之上 + Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedContainer( + duration: const Duration(milliseconds: 0), + height: + _showFloatingAppBar ? (statusBar + _floatingBarHeight) : 0, + decoration: BoxDecoration( + color: + _showFloatingAppBar + ? h_AppBarColor() + : Colors.transparent, + boxShadow: + _showFloatingAppBar + ? [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 6, + ), + ] + : [], + ), + child: SafeArea( + bottom: false, + child: Opacity( + opacity: _showFloatingAppBar ? 1.0 : 0.0, + child: SizedBox( + height: _floatingBarHeight, + child: const SizedBox() + ), + ), + ), + ), + ), + Positioned( + top: statusBar + 14, + width: screenWidth(context), + child: Center(child: + Text( + '秦港-相关方安全管理', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ),) + ), + // 固定在最上层的图标(位于 AppBar 之上),保证它们不会随滚动移动 + _buildFixedTopIcons(context), + ], + ), + ), + ); + } + + // 固定在屏幕右上角的图标(不会随页面滚动) + Widget _buildFixedTopIcons(BuildContext context) { + final double statusBar = MediaQuery.of(context).padding.top; + // 固定图标距离顶部的偏移(在 banner 内时可调小) + final double topOffset = statusBar + 12; + + return Positioned( + top: topOffset, + right: 12, + child: Row( + children: [ + GestureDetector( + onTap: startScan, + child: Container( + width: 38, + height: 38, + alignment: Alignment.center, + child: Image.asset( + "assets/icon-apps/home_saoyisao.png", + width: 22, + height: 22, + ), + ), + ), + ], + ), + ); + } + + Widget _buildNotificationBar(double notificationHeight) { + return Material( + color: Colors.transparent, + child: Container( + height: notificationHeight, + decoration: BoxDecoration( + color: const Color(0xFFE6F5FF), // 浅蓝 + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.white, width: 1), // 白色边框 + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // const SizedBox(height: 40), + Row( + children: [ + const SizedBox(width: 12), + // 左侧图标 + Image.asset('assets/images/ico8.png', width: 30, height: 25), + const SizedBox(width: 12), + + // 中间可滚动的文本区域(使用垂直 PageView) + Expanded( + child: SizedBox( + height: notificationHeight, + child: PageView.builder( + controller: _notifPageController, + scrollDirection: Axis.vertical, + physics: const NeverScrollableScrollPhysics(), + itemCount: _notifications.length, + itemBuilder: (context, index) { + final text = _notifications[index]; + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.black87, + fontSize: 14, + ), + ), + ), + ); + }, + ), + ), + ), + + // 右侧箭头 + const Icon(Icons.chevron_right, color: Colors.black26), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ); + } + + // 构建顶部 Banner + Widget _buildBannerSection(double bannerHeight) { + return Stack( + children: [ + // 背景图片 + Image.asset( + "assets/images/banner.png", + width: MediaQuery.of(context).size.width, + height: bannerHeight, + fit: BoxFit.cover, + ), + + // 这里保留 banner 内的额外装饰(如果需要) + ], + ); + } + + Widget _buildIconSection(BuildContext context) { + // 判断是否有第二行 + final hasSecondRow = buttonInfos.length > 4; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 5), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)), + ], + ), + child: widget.isChooseFirm + ? Column( + children: [ + // 第一行(4 列等分) + Row( + children: List.generate(4, (i) { + final idx = i; + if (idx < buttonInfos.length) { + return Expanded( + child: Center(child: _buildIconButton(buttonInfos[idx], context)), + ); + } else { + return const Expanded(child: SizedBox()); + } + }), + ), + + if (hasSecondRow) const SizedBox(height: 20), + + // 第二行(仍然 4 列等分;不足的用占位填充) + if (hasSecondRow) + Row( + children: List.generate(4, (i) { + final idx = 4 + i; // 第二行从索引4开始 + if (idx < buttonInfos.length) { + return Expanded( + child: Center(child: _buildIconButton(buttonInfos[idx], context)), + ); + } else { + return const Expanded(child: SizedBox()); + } + }), + ), + ], + ) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/ico1.png', width: 100, height: 100), + const SizedBox(height: 10), + CustomButton( + text: '点击入职企业', + onPressed: () { + pushPage(FirmListPage(), context); + }, + ) + ], + ), + ), + ); + } + + + // 构建单个图标按钮(保持原样) + Widget _buildIconButton(Map info, BuildContext context) { + return GestureDetector( + onTap: () { + _handleIconTap(info['title']); + }, + child: Column( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: const Color(0xFFE8F4FD), + borderRadius: BorderRadius.circular(25), + ), + child: Center( + child: Image.asset(info['icon'], width: 30, height: 30), + ), + ), + const SizedBox(height: 6), + SizedBox( + width: 70, + child: Text( + info['title'], + style: const TextStyle(fontSize: 12, color: Colors.black87), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + // 处理图标点击 + void _handleIconTap(String title) { + switch (title) { + case "单位管理": + pushPage(UnitTabPage(), context); + break; + case "现场监管": + break; + case "危险作业": + break; + case "隐患处理": + break; + case "安环检查": + break; + case "口门门禁": + break; + case "入港培训": + break; + default: + break; + } + } + + // 构建工作统计区域 - 新版设计 + Widget _buildWorkStatsSection() { + double buttonHeight = 45.0; + return Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0, 2)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 切换按钮 + Container( + width: screenWidth(context), + height: buttonHeight, + decoration: BoxDecoration( + // color: Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // 手机端按钮 + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _isMobileSelected = true; + }); + }, + child: Container( + decoration: BoxDecoration( + color: + _isMobileSelected + ? const Color(0xFFE9F4FE) + : Colors.white, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + ), + ), + child: Center( + child: Text( + "手机端", + style: TextStyle( + fontSize: 14, + color: + _isMobileSelected ? Colors.black : Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + Image.asset( + 'assets/images/${_isMobileSelected ? 'img2.png' : 'img1.png'}', + fit: BoxFit.cover, + height: buttonHeight, + width: 30, + ), + // 电脑端按钮 + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _isMobileSelected = false; + }); + }, + child: Container( + decoration: BoxDecoration( + color: + !_isMobileSelected + ? const Color(0xFFE9F4FE) + : Colors.white, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(12), + ), + ), + child: Center( + child: Text( + "电脑端", + style: TextStyle( + fontSize: 15, + color: + !_isMobileSelected ? Colors.black : Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.all(15), + child: Column( + children: [ + // 今日工作项 + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 0), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: "今日有工作项", + style: TextStyle(fontSize: 16, color: Colors.black87), + ), + TextSpan( + text: " ${workStats['total']}", + style: const TextStyle( + fontSize: 16, + color: Color(0xFF2A75F8), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: "个", + style: TextStyle(fontSize: 16, color: Colors.black87), + ), + ], + ), + ), + ), + + const SizedBox(height: 15), + // 第三行:已处理和待处理工作项 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "已处理工作项:", + style: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + TextSpan( + text: " ${workStats['processed']}", + style: TextStyle( + fontSize: 14, + color: Colors.greenAccent, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: "个", + style: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: "待处理工作项:", + style: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + TextSpan( + text: " ${workStats['processed']}", + style: TextStyle( + fontSize: 14, + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: "个", + style: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + // 构建检查清单区域 + Widget _buildCheckListSection() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ' 待办清单', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(), + ], + ), + SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 6, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [...checkLists.map((item) => _buildCheckListItem(item))], + ), + ), + ], + ); + } + + // 构建检查清单项 + Widget _buildCheckListItem(Map item) { + return Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey[200]!, width: 1)), + ), + child: Row( + children: [ + // 内容区域 - 使用Expanded确保不溢出 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 标题使用Flexible防止溢出 + Flexible( + child: Text( + item['title'], + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, // 添加省略号 + maxLines: 1, // 限制为1行 + ), + ), + const SizedBox(width: 8), // 添加间距 + Text( + item['type'], + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + ], + ), + const SizedBox(height: 8), // 替代Divider的间距 + // 底部信息行 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 类型标签使用Flexible + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "类型:${item['type']}", + style: const TextStyle( + fontSize: 12, + color: Colors.blue, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: 8), // 添加间距 + // 时间使用Text + Flexible( + child: Text( + "时间:${item['time']}", + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/home/scan_page.dart b/lib/pages/home/scan_page.dart new file mode 100644 index 0000000..684c755 --- /dev/null +++ b/lib/pages/home/scan_page.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; + +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; + +// 扫码类型 +enum ScanType { + /// 入职 + Onboarding, +} + +class ScanPage extends StatefulWidget { + // const ScanPage({Key? key}) : super(key: key,); + const ScanPage({super.key, required this.type}); + final ScanType type; + @override + State createState() => _ScanPageState(); +} + +class _ScanPageState extends State { + final MobileScannerController _controller = MobileScannerController(); + bool _torchOn = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _scanFromGallery() async { + final picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + if (image == null) return; + + try { + // ★ 新版:返回 BarcodeCapture? + final capture = await _controller.analyzeImage(image.path); + if (capture != null && capture.barcodes.isNotEmpty) { + final code = capture.barcodes.first.rawValue ?? ''; + _showResult(code); + } else { + _showResult('未识别到二维码/条码'); + } + } catch (e) { + _showResult('扫描失败:$e'); + } + } + + void _showResult(String result) { + if (widget.type == ScanType.Onboarding) { + final json = jsonDecode(result); + Navigator.pop(context, json); + } + + } + + // 人脸识别跳转 + void goToFace(Map stuInfo) async { + print('navigate to face with $stuInfo'); + // final passed = await pushPage( + // FaceRecognitionPage( + // studentId: stuInfo['STUDENT_ID'], + // data: { + // 'VIDEOCOURSEWARE_ID': stuInfo['VIDEOCOURSEWARE_ID'], + // 'CURRICULUM_ID': stuInfo['CURRICULUM_ID'], + // 'CHAPTER_ID': stuInfo['CHAPTER_ID'], + // 'CLASS_ID': stuInfo['CLASS_ID'], + // 'STUDENT_ID':stuInfo['STUDENT_ID'], + // }, + // mode: FaceMode.scan, + // ), + // context, + // ); + // if (passed == true) { + // ToastUtil.showSuccess(context, '验证成功'); + // Navigator.pop(context); + // } else { + // ToastUtil.showError(context, '验证失败'); + // Navigator.pop(context); + // } + } + + // 跳转到清单页面 + void goToList({required String listId, required String listName}) { + print('navigate to list: $listId, name: $listName'); + // Navigator.pop(context, Animation); + // pushPage(RiskListPage(1, listId, ""), context); + } + + @override + Widget build(BuildContext context) { + // 中心扫描框大小 + const double scanSize = 250; + final Size screen = MediaQuery.of(context).size; + final double left = (screen.width - scanSize) / 2; + final double top = (screen.height - scanSize) / 3 - kToolbarHeight; + // 因为 SafeArea + AppBar 占了高度,所以减去 toolbar 高度 + const double cornerSize = 20.0; // 角标正方形区域大小 + const double strokeWidth = 4.0; // 边线宽度 + return Scaffold( + appBar: MyAppbar( + title: "二维码/条码扫描", + actions: [ + TextButton( + onPressed: _scanFromGallery, + child: const Text( + "相册", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ], + ), + body: Stack( + children: [ + // 1. 摄像头预览 + MobileScanner( + controller: _controller, + onDetect: (capture) { + for (final barcode in capture.barcodes) { + final code = barcode.rawValue; + if (code != null && mounted) { + _controller.stop(); + _showResult(code); + break; + } + } + }, + ), + + // 2. 半透明遮罩 + // 顶部 + // 1. 顶部遮罩 + Positioned( + left: 0, + right: 0, + top: 0, + height: top, + // 从顶到底部到扫描框上边缘 + child: Container(color: Colors.black54), + ), + + // 2. 底部遮罩 + Positioned( + left: 0, + right: 0, + top: top + scanSize, + // 从扫描框下边缘开始 + bottom: 0, + child: Container(color: Colors.black54), + ), + + // 3. 左侧遮罩 + Positioned( + left: 0, + top: top, + width: left, + // 从屏幕左侧到扫描框左边缘 + height: scanSize, + // 和扫描框一样高 + child: Container(color: Colors.black54), + ), + + // 4. 右侧遮罩 + Positioned( + left: left + scanSize, + top: top, + right: 0, + height: scanSize, + // 和扫描框一样高 + child: Container(color: Colors.black54), + ), + + // 3. 扫描框四个角 + // 左上 + Positioned( + left: left, + top: top, + child: _corner(size: cornerSize, stroke: strokeWidth), + ), + // 右上 + Positioned( + left: left + scanSize - cornerSize, + top: top, + child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 1), + ), + // 左下 + Positioned( + left: left, + top: top + scanSize - cornerSize, + child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 3), + ), + // 右下 + Positioned( + left: left + scanSize - cornerSize, + top: top + scanSize - cornerSize, + child: _corner(size: cornerSize, stroke: strokeWidth, rotation: 2), + ), + + // 闪光灯按钮 + Positioned( + left: (screen.width - 40) / 2, + top: top + scanSize - 60, + child: IconButton( + iconSize: 32, + color: Colors.white, + icon: Icon( + _torchOn + ? Icons.flashlight_off_outlined + : Icons.flashlight_on_outlined, + ), + onPressed: () { + _controller.toggleTorch(); + setState(() { + _torchOn = !_torchOn; + }); + }, + ), + ), + ], + ), + ); + } + + /// 角装饰:一个 L 形的蓝色粗边 + Widget _corner({ + double size = 20, + double stroke = 4, + int rotation = 0, // 0=左上, 1=右上, 2=右下, 3=左下 + }) { + return Transform.rotate( + angle: rotation * math.pi / 2, + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Colors.blue, width: stroke), + left: BorderSide(color: Colors.blue, width: stroke), + ), + ), + ), + ); + } +} diff --git a/lib/pages/home/unit/unit_tab_page.dart b/lib/pages/home/unit/unit_tab_page.dart new file mode 100644 index 0000000..6e8c399 --- /dev/null +++ b/lib/pages/home/unit/unit_tab_page.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/work_tab_icon_grid.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/customWidget/IconBadgeButton.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:qhd_prevention/common/route_aware_state.dart'; + +class UnitTabPage extends StatefulWidget { + const UnitTabPage({super.key}); + + @override + State createState() => _UnitTabPageState(); +} + +class _UnitTabPageState extends RouteAwareState { + late List> buttonInfos = [ + { + "icon": "assets/images/unit_ico1.png", + "title": "服务单位管理", + "unreadCount": 0, + } + ,{ + "icon": "assets/images/unit_ico2.png", + "title": "就职单位管理", + "unreadCount": 0, + } + ]; + + @override + void initState() { + super.initState(); + } + Future onVisible() async { + _getData(); + } + Future _getData() async { + + } + + void _handleIconTap(int index) async { + switch (index) { + case 0: + break; + } + _getData(); + } + @override + Widget build(BuildContext context) { + double bannerHeight = 618/1125 * MediaQuery.of(context).size.width; + const double iconSectionHeight = 150.0; + const double iconOverlapBanner = 30.0; // 图标区覆盖 banner 的高度 + return PopScope( + canPop: false, + 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 height) { + return Stack( + children: [ + // 背景图片 + Image.asset( + "assets/images/unit_banner.jpg", + width: MediaQuery.of(context).size.width, + height: height, + fit: BoxFit.cover, + ), + ], + ); + } + 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/userinfo_page.dart b/lib/pages/home/userinfo_page.dart new file mode 100644 index 0000000..f00e147 --- /dev/null +++ b/lib/pages/home/userinfo_page.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +import '../../http/ApiService.dart'; + +class UserinfoPage extends StatefulWidget { + const UserinfoPage({super.key}); + + @override + State createState() => _UserinfoPageState(); +} + +class _UserinfoPageState extends State { + + Map user = {}; + + @override + void initState() { + // TODO: implement initState + super.initState(); + + _getUserInfo(); + } + + Future _getUserInfo() async { + try { + LoadingDialogHelper.show(); + // final result = await BasicInfoApi.getUserMessage(SessionService.instance.accountId ?? ""); + final result = await AuthApi.getUserData(); + LoadingDialogHelper.hide(); + if (result['success']) { + setState(() { + user= result['data']; + }); + + }else{ + ToastUtil.showNormal(context, "加载数据失败"); + // _showMessage('加载数据失败'); + } + } catch (e) { + LoadingDialogHelper.hide(); + // 出错时可以 Toast 或者在页面上显示错误状态 + print('加载数据失败:$e'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: "人员信息"), + body: SafeArea( + child: ListView( + children: [ + _userItemCell("姓名", user["name"]??"", false), + Divider(height: 1), + _userItemCell("手机号", user["phone"]??"", false), + Divider(height: 1), + _userItemCell("部门", user["departmentName"]??"", false), + Divider(height: 1), + _userItemCell("职务", user["postName"]??"", false), + Divider(height: 1), + _userItemCell("职务级别", user["rankLevelName"]??"", false), + Divider(height: 1), + + ], + ), + ), + ); + } + + Widget _userItemCell(final String title, final String detail, bool isLast) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 15), + decoration: + isLast + ? const BoxDecoration( + color: Colors.white, + border: Border( + bottom: BorderSide(color: Colors.grey, width: 0), + ), + ) + : const BoxDecoration( + color: Colors.white, + + border: Border( + bottom: BorderSide(color: Colors.grey, width: 0.5), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(title, style: const TextStyle(fontSize: 16, )), + Flexible( + child: Text( + detail, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500,color: Colors.grey), + textAlign: TextAlign.right, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + void _showMessage(String msg) { + ToastUtil.showNormal(context, msg); + } + +} diff --git a/lib/pages/main_tab.dart b/lib/pages/main_tab.dart new file mode 100644 index 0000000..f64312d --- /dev/null +++ b/lib/pages/main_tab.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/home/home_page.dart'; +import 'package:qhd_prevention/pages/mine/mine_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/heartbeat_service.dart'; + +/// 用于向子树公布当前 tab 索引 +class CurrentTabNotifier extends InheritedWidget { + final int currentIndex; + + const CurrentTabNotifier({ + required this.currentIndex, + required Widget child, + Key? key, + }) : super(key: key, child: child); + + static CurrentTabNotifier? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + @override + bool updateShouldNotify(covariant CurrentTabNotifier oldWidget) { + return oldWidget.currentIndex != currentIndex; + } +} + +class MainPage extends StatefulWidget { + const MainPage({Key? key, required this.isChooseFirm}) : super(key: key); + final bool isChooseFirm; + + @override + _MainPageState createState() => _MainPageState(); +} + +class _MainPageState extends State with WidgetsBindingObserver { + int _currentIndex = 0; + final GlobalKey _homeKey = GlobalKey(); + + // final GlobalKey _appKey = GlobalKey(); + final GlobalKey _mineKey = GlobalKey(); + late List _pages; + late List _tabVisibility; // 存储每个Tab的显示状态 + + final List _titles = ['首页', '通知', '我的']; + + @override + void initState() { + super.initState(); + // 注册生命周期监听 + WidgetsBinding.instance.addObserver(this); + // 初始化所有Tab + _tabVisibility = [true, true, true]; + _pages = [HomePage(key: _homeKey, isChooseFirm: widget.isChooseFirm,), MinePage(key: _mineKey)]; + // 启动心跳服务 + // HeartbeatService().start(); + } + + @override + void dispose() { + // BadgeManager().removeListener(_onBadgeChanged); + // 移除生命周期监听 + WidgetsBinding.instance.removeObserver(this); + + // 停止心跳服务 + HeartbeatService().stop(); + + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.resumed: + // 应用回到前台,恢复心跳 + HeartbeatService().resume(); + break; + case AppLifecycleState.paused: + case AppLifecycleState.inactive: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + // 应用进入后台,暂停心跳 + HeartbeatService().pause(); + break; + } + } + + Widget _buildIconWithBadge({required Widget icon, required int badgeCount}) { + if (badgeCount <= 0) return icon; + return Stack( + clipBehavior: Clip.none, + children: [ + icon, + Positioned( + right: -12, + top: -4, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(8), + ), + constraints: const BoxConstraints(minWidth: 16, minHeight: 16), + child: Center( + child: Text( + '$badgeCount', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + height: 1, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + // final bm = BadgeManager(); + + // 构建可见的底部导航项 + final List visibleItems = []; + final List visiblePages = []; + + for (int i = 0; i < _tabVisibility.length; i++) { + if (_tabVisibility[i]) { + switch (i) { + case 0: + visibleItems.add( + BottomNavigationBarItem( + icon: Image.asset( + 'assets/tabbar/basics.png', + width: 24, + height: 24, + ), + activeIcon: Image.asset( + 'assets/tabbar/basics_cur.png', + width: 24, + height: 24, + ), + label: '首页', + ), + ); + visiblePages.add(_pages[i]); + break; + case 1: + visibleItems.add( + BottomNavigationBarItem( + icon: Image.asset( + 'assets/tabbar/my.png', + width: 24, + height: 24, + ), + activeIcon: Image.asset( + 'assets/tabbar/my_cur.png', + width: 24, + height: 24, + ), + label: '我的', + ), + ); + visiblePages.add(_pages[i]); + break; + break; + } + } + } + + // 将当前索引映射到可见Tab的索引 + int getVisibleIndex(int originalIndex) { + int visibleIndex = 0; + for (int i = 0; i <= originalIndex; i++) { + if (_tabVisibility[i]) { + if (i == originalIndex) return visibleIndex; + visibleIndex++; + } + } + return 0; // 默认返回第一个可见Tab + } + + final visibleCurrentIndex = getVisibleIndex(_currentIndex); + + return CurrentTabNotifier( + currentIndex: _currentIndex, + child: Scaffold( + appBar: null, + body: IndexedStack(index: visibleCurrentIndex, children: visiblePages), + bottomNavigationBar: + visibleItems.length >= 2 + ? BottomNavigationBar( + currentIndex: visibleCurrentIndex, + type: BottomNavigationBarType.fixed, + selectedItemColor: Colors.blue, + unselectedItemColor: Colors.grey, + onTap: (visibleIndex) { + int originalIndex = 0; + int count = 0; + for (int i = 0; i < _tabVisibility.length; i++) { + if (_tabVisibility[i]) { + if (count == visibleIndex) { + originalIndex = i; + break; + } + count++; + } + } + setState(() => _currentIndex = originalIndex); + }, + items: visibleItems, + ) + : null, // 如果没有可见的Tab,隐藏底部导航栏 + ), + ); + } +} diff --git a/lib/pages/mine/face_ecognition_page.dart b/lib/pages/mine/face_ecognition_page.dart new file mode 100644 index 0000000..e553e56 --- /dev/null +++ b/lib/pages/mine/face_ecognition_page.dart @@ -0,0 +1,502 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; + +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart' as ph; +import 'package:qhd_prevention/constants/app_enums.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/custom_button.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +// 在类最上面(State 内)加入一个 channel 常量 +const MethodChannel _platformChan = MethodChannel('qhd_prevention/permissions'); + +/// 人脸识别模式 +enum FaceMode { initSave, setUpdata, study, scan } + +class FaceRecognitionPage extends StatefulWidget { + final String studentId; + final Map data; + final FaceMode mode; + + const FaceRecognitionPage({ + Key? key, + required this.studentId, + required this.data, + this.mode = FaceMode.study, + }) : super(key: key); + + @override + _FaceRecognitionPageState createState() => _FaceRecognitionPageState(); +} + +class _FaceRecognitionPageState extends State + with WidgetsBindingObserver { + CameraController? _cameraController; + Timer? _timer; + int _attempts = 0; + + static const int _maxAttempts = 8; + static const Duration _interval = Duration(seconds: 2); + String _errMsg = ''; + + bool get _isManualMode => + (widget.mode == FaceMode.setUpdata || widget.mode == FaceMode.initSave); + + bool _isInitializing = false; + bool _isTaking = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + // 延迟到首帧渲染后再请求权限并初始化相机,避免 iOS 在还未准备好时错过系统弹窗 + WidgetsBinding.instance.addPostFrameCallback((_) { + _initCameraWithPermission(); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _timer?.cancel(); + try { + _cameraController?.dispose(); + } catch (_) {} + super.dispose(); + } + + // 生命周期:后台/前台切换时处理相机释放/恢复 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + return; + } + if (state == AppLifecycleState.inactive) { + // 可选:释放相机,节省资源 + try { + _cameraController?.dispose(); + Navigator.pop(context); + } catch (_) {} + _cameraController = null; + } else if (state == AppLifecycleState.resumed) { + // 恢复时重新初始化相机(并会再次请求权限) + if (!_isInitializing) { + _initCameraWithPermission(); + } + } + } + + /// 请求 camera 权限(iOS:优先走原生 AVCaptureDevice.requestAccess;Android:使用 permission_handler) + Future _requestCameraPermission() async { + try { + // iOS:使用原生 API 强制唤起系统授权弹窗 + if (Platform.isIOS) { + try { + final dynamic res = await _platformChan.invokeMethod( + 'requestCameraAccess', + ); + // 原生返回 true/false + if (res == true) { + debugPrint('[FaceRecognition] iOS native camera access granted'); + return true; + } else { + debugPrint('[FaceRecognition] iOS native camera access denied'); + // 如果被拒绝(非永久还是永久都返回 false),再根据 permission_handler 的状态做后续提示/引导 + final status = await ph.Permission.camera.status; + if (status.isPermanentlyDenied) { + await _showPermissionDialog(permanent: true); + } else { + await _showPermissionDialog(permanent: false); + } + return false; + } + } on PlatformException catch (e) { + debugPrint( + '[FaceRecognition] platform channel error: $e — fallback to permission_handler', + ); + // 如果 platform channel 异常,回退到 permission_handler + } + } + + // 非 iOS 或 platform 调用失败时走 permission_handler(保证 Android 正常) + final result = await ph.Permission.camera.request(); + debugPrint('[FaceRecognition] permission_handler camera result: $result'); + + if (result.isGranted) return true; + if (result.isPermanentlyDenied) { + await _showPermissionDialog(permanent: true); + return false; + } + if (result.isDenied) { + await _showPermissionDialog(permanent: false); + return false; + } + if (result.isRestricted) { + if (mounted) ToastUtil.showNormal(context, '相机权限受限,无法使用本功能'); + return false; + } + if (result.isLimited) { + if (mounted) ToastUtil.showNormal(context, '相机权限受限(limited)'); + return false; + } + return false; + } catch (e) { + debugPrint('[FaceRecognition] permission request error: $e'); + if (mounted) ToastUtil.showNormal(context, '请求相机权限时出错'); + return false; + } + } + + /// 显示权限被拒/永久拒绝对话框(区分 permanent) + Future _showPermissionDialog({required bool permanent}) async { + if (!mounted) return; + await CustomAlertDialog.showConfirm( + context, + title: '需要相机权限', + content: + permanent + ? '检测到相机权限已被永久拒绝,请到系统设置中打开相机权限以继续使用人脸识别功能。' + : '相机权限被拒绝,是否重试或前往设置打开权限?', + cancelText: '取消', + onConfirm: () async { + try { + final retry = await ph.Permission.camera.request(); + debugPrint('[FaceRecognition] retry request result: $retry'); + if (retry.isGranted) { + if (mounted) await _initCamera(); + } else if (retry.isPermanentlyDenied) { + try { + await ph.openAppSettings(); + } catch (e) { + debugPrint('[FaceRecognition] openAppSettings error: $e'); + if (mounted) ToastUtil.showNormal(context, '无法打开设置,请手动前往系统设置授权'); + } + } else { + if (mounted) ToastUtil.showNormal(context, '相机权限未授予'); + } + } catch (e) { + debugPrint('[FaceRecognition] retry request error: $e'); + } + }, + ); + } + + /// 初始化:先请求权限,再打开相机 + Future _initCameraWithPermission() async { + if (!mounted) return; + final ok = await _requestCameraPermission(); + if (!ok) return; + await _initCamera(); + } + + Future _initCamera() async { + if (_isInitializing) return; + _isInitializing = true; + + try { + final cams = await availableCameras(); + + CameraDescription? front; + try { + front = cams.firstWhere( + (c) => c.lensDirection == CameraLensDirection.front, + ); + } catch (_) { + if (cams.isNotEmpty) front = cams.first; + } + + if (front == null) { + if (!mounted) return; + ToastUtil.showError(context, '未检测到可用摄像头'); + _isInitializing = false; + return; + } + + _cameraController = CameraController( + front, + ResolutionPreset.medium, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.yuv420, + ); + + await _cameraController!.initialize(); + + // 尽量关闭闪光 + try { + await _cameraController!.setFlashMode(FlashMode.off); + } catch (_) {} + + if (!mounted) return; + setState(() {}); + + // 启动自动拍照定时器(若为自动模式) + _timer?.cancel(); + _attempts = 0; + if (!_isManualMode) { + _timer = Timer.periodic(_interval, (_) => _captureAndUpload()); + } + } catch (e, st) { + debugPrint('[FaceRecognition] init camera error: $e\n$st'); + if (mounted) { + ToastUtil.showError(context, '初始化摄像头失败'); + } + } finally { + _isInitializing = false; + } + } + + /// 自动模式:定时拍照并上传 + Future _captureAndUpload() async { + if (_isManualMode) return; + if (_cameraController == null || !_cameraController!.value.isInitialized) + return; + if (_attempts >= _maxAttempts) return _onTimeout(); + + if (_isTaking) return; + _isTaking = true; + _attempts++; + + // try { + // try { + // await _cameraController!.setFlashMode(FlashMode.off); + // } catch (_) {} + // + // final XFile pic = await _cameraController!.takePicture(); + // var res = {}; + // switch(widget.mode) { + // case FaceMode.study: + // res = await ApiService.getStudyUserFace(pic.path, widget.data); + // break; + // case FaceMode.setUpdata: + // res = await ApiService.getUpdataUserFace(pic.path, widget.data); + // break; + // case FaceMode.scan: + // res = await ApiService.getScanUserFace(pic.path, widget.data); + // break; + // } + // if (res['result'] == 'success') { + // _onSuccess(); + // } else { + // if (!mounted) return; + // setState(() { + // _errMsg = (res['msg'] ?? '').toString(); + // }); + // } + // } catch (e, st) { + // debugPrint('[FaceRecognition] capture error: $e\n$st'); + // // 忽略单次异常,等待下一次尝试 + // } finally { + // _isTaking = false; + // } + } + + /// 手动拍照并上传 + Future _captureAndReload() async { + if (_cameraController == null || !_cameraController!.value.isInitialized) + return; + if (_isTaking) return; + _isTaking = true; + _showLoading(); + + try { + // 再次确认 camera 权限(以防用户运行时撤销) + final status = await ph.Permission.camera.status; + if (!status.isGranted) { + final ok = await _requestCameraPermission(); + if (!ok) { + _hideLoading(); + _isTaking = false; + return; + } + } + + try { + await _cameraController!.setFlashMode(FlashMode.off); + } catch (_) {} + + final XFile pic = await _cameraController!.takePicture(); + if (widget.mode == FaceMode.initSave) { + _hideLoading(); + _onSuccess(pic.path); + return; + } + final raw = await FileApi.uploadFile( + pic.path, + UploadFileType.facialRecognitionImages, + '', + ); + if (raw['success']) { + String imagePath = raw['data']['filePath']; + final res = await AuthApi.reloadMyFace(imagePath); + _hideLoading(); + + if (res['success']) { + _onSuccess(imagePath); + } else { + ToastUtil.showError(context, '验证失败,请重试'); + } + } else { + // _showMessage('反馈提交失败'); + ToastUtil.showError(context, '验证失败,请重试'); + } + } catch (e, st) { + debugPrint('[FaceRecognition] manual capture error: $e\n$st'); + _hideLoading(); + ToastUtil.showError(context, '拍照失败,请重试'); + } finally { + _isTaking = false; + } + } + + void _onSuccess(String filePath) { + _timer?.cancel(); + if (widget.mode == FaceMode.initSave) { + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) Navigator.of(context).pop(filePath); + }); + return; + } + if (widget.mode == FaceMode.setUpdata) { + ToastUtil.showSuccess(context, '已更新人脸信息'); + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) Navigator.of(context).pop(true); + }); + return; + } + + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) Navigator.of(context).pop(true); + }); + } + + void _onTimeout() { + _timer?.cancel(); + ToastUtil.showError(context, '人脸超时,请重新识别!'); + Future.delayed(const Duration(seconds: 3), () { + if (mounted) Navigator.of(context).pop(false); + }); + } + + void _showLoading() { + if (!mounted) return; + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); + } + + void _hideLoading() { + if (!mounted) return; + if (Navigator.canPop(context)) Navigator.pop(context); + } + + void _showToast(String msg) { + ToastUtil.showNormal(context, msg); + } + + @override + Widget build(BuildContext context) { + if (_cameraController == null || !_cameraController!.value.isInitialized) { + return const Scaffold( + backgroundColor: Colors.white, + body: Center(child: CircularProgressIndicator()), + ); + } + + final previewSize = _cameraController!.value.previewSize!; + final previewAspect = previewSize.height / previewSize.width; + final radius = (screenWidth(context) - 100) / 2; + + return Scaffold( + backgroundColor: Colors.white, + appBar: const MyAppbar(title: '人脸认证'), + body: Stack( + children: [ + Positioned.fill(child: Container(color: Colors.white)), + Transform.translate( + offset: const Offset(0, -100), + child: Stack( + children: [ + Center( + child: ClipOval( + child: AspectRatio( + aspectRatio: previewAspect, + child: CameraPreview(_cameraController!), + ), + ), + ), + Positioned.fill( + child: CustomPaint( + painter: _WhiteMaskPainter(radius: radius), + ), + ), + ], + ), + ), + + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '请将人脸置于圆圈内', + style: TextStyle(fontSize: 16, color: Colors.black87), + textAlign: TextAlign.center, + ), + if (_errMsg.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Text( + _errMsg, + style: const TextStyle(fontSize: 14, color: Colors.red), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + if (_isManualMode) + CustomButton( + text: '拍照/上传', + backgroundColor: Colors.blue, + onPressed: _captureAndReload, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _WhiteMaskPainter extends CustomPainter { + final double radius; + + _WhiteMaskPainter({required this.radius}); + + @override + void paint(Canvas canvas, Size size) { + canvas.saveLayer(Offset.zero & size, Paint()); + canvas.drawRect(Offset.zero & size, Paint()..color = Colors.white); + canvas.drawCircle( + size.center(Offset.zero), + radius, + Paint()..blendMode = BlendMode.clear, + ); + canvas.restore(); + } + + @override + bool shouldRepaint(covariant CustomPainter old) => false; +} diff --git a/lib/pages/mine/forgot_pwd_page.dart b/lib/pages/mine/forgot_pwd_page.dart new file mode 100644 index 0000000..9c6fc5b --- /dev/null +++ b/lib/pages/mine/forgot_pwd_page.dart @@ -0,0 +1,345 @@ +import 'dart:async'; +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/pages/my_appbar.dart'; +import 'package:qhd_prevention/pages/user/CustomInput.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'; +import 'package:qhd_prevention/services/auth_service.dart'; // 如果你 APIs 在别处,请替换/调整 + +class ForgotPwdPage extends StatefulWidget { + const ForgotPwdPage(this.type, {super.key}); + + final String type; + @override + State createState() => _ForgotPwdPageState(); +} + +class _ForgotPwdPageState extends State { + final _formKey = GlobalKey(); + + final _phoneController = TextEditingController(); + final _codeController = TextEditingController(); + final _newPwdController = TextEditingController(); + final _confirmPwdController = TextEditingController(); + + bool _obscureNew = true; + bool _obscureConfirm = true; + + String textString = + "为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + + // 验证码倒计时 + Timer? _timer; + int _secondsLeft = 0; + bool _isSending = false; + + @override + void initState() { + super.initState(); + // 根据 type 设置提示文案(保持原逻辑) + switch (widget.type) { + case "0": + textString = "密码长度8-18位,需包含数字、字母、英文符号至少2种或以上元素"; + break; + case "1": + textString = + "检测到您的密码为弱密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "2": + textString = + "检测到您30天内未修改密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "3": + textString = + "检测到您的密码为弱密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "4": + textString = + "检测到您30天内未修改密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + } + + // 可选:预填手机号(如果 session 中有) + _phoneController.text = SessionService.instance.loginPhone ?? ''; + } + + @override + void dispose() { + _timer?.cancel(); + _phoneController.dispose(); + _codeController.dispose(); + _newPwdController.dispose(); + _confirmPwdController.dispose(); + super.dispose(); + } + + bool get _canSend => _secondsLeft == 0 && !_isSending; + + String get _sendText => _secondsLeft > 0 ? '$_secondsLeft s后可重发' : '发送验证码'; + + void _startCountdown(int seconds) { + _timer?.cancel(); + setState(() { + _secondsLeft = seconds; + }); + _timer = Timer.periodic(const Duration(seconds: 1), (t) { + if (!mounted) return; + setState(() { + _secondsLeft--; + if (_secondsLeft <= 0) { + _timer?.cancel(); + _secondsLeft = 0; + } + }); + }); + } + + // 手机号简单校验(11位) + bool _isPhoneValid(String phone) { + final RegExp phoneReg = RegExp(r'^\d{11}$'); + return phoneReg.hasMatch(phone); + } + + // 密码复杂度校验 + bool isPasswordValid(String password) { + final hasUpperCase = RegExp(r'[A-Z]'); + final hasLowerCase = RegExp(r'[a-z]'); + final hasNumber = RegExp(r'[0-9]'); + final hasSpecialChar = RegExp(r'[!@#\$%\^&\*\(\)_\+\-=\[\]\{\};:"\\|,.<>\/\?~`]'); + return hasUpperCase.hasMatch(password) && + hasLowerCase.hasMatch(password) && + hasNumber.hasMatch(password) && + hasSpecialChar.hasMatch(password); + } + + Future _sendVerificationCode() async { + final phone = _phoneController.text.trim(); + if (phone.isEmpty) { + ToastUtil.showNormal(context, '请输入手机号'); + return; + } + if (!_isPhoneValid(phone)) { + ToastUtil.showNormal(context, '请输入有效手机号(11位)'); + return; + } + if (!_canSend) return; + + setState(() => _isSending = true); + LoadingDialogHelper.show(); + try { + final res = await BasicInfoApi.sendRegisterSms({'phone': phone}); + LoadingDialogHelper.hide(); + if (res['success'] == true) { + ToastUtil.showNormal(context, '验证码已发送'); + _startCountdown(60); + } else { + ToastUtil.showNormal(context, res?['message'] ?? '发送验证码失败'); + } + } catch (e) { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '发送验证码失败: $e'); + } finally { + if (mounted) setState(() => _isSending = false); + } + } + + Future _handleSubmit() async { + if (!(_formKey.currentState?.validate() ?? false)) return; + + final phone = _phoneController.text.trim(); + final code = _codeController.text.trim(); + final newPwd = _newPwdController.text.trim(); + final confirm = _confirmPwdController.text.trim(); + + if (phone.isEmpty || code.isEmpty || newPwd.isEmpty || confirm.isEmpty) { + ToastUtil.showNormal(context, '请完整填写表单'); + return; + } + + if (!_isPhoneValid(phone)) { + ToastUtil.showNormal(context, '请输入有效手机号(11位)'); + return; + } + + if (newPwd != confirm) { + ToastUtil.showNormal(context, '两次输入的密码不一致'); + return; + } + + if (newPwd.length < 8) { + ToastUtil.showNormal(context, '新密码需要大于8位'); + return; + } + + if (newPwd.length > 32) { + ToastUtil.showNormal(context, '新密码需要小于32位'); + return; + } + + if (!isPasswordValid(newPwd)) { + ToastUtil.showNormal(context, '新密码必须包含大小写字母、数字和特殊符号。'); + return; + } + + LoadingDialogHelper.show(); + try { + final res = await AuthApi.passwordRecover({'phone': phone, 'phoneCode': code, 'newPassword': newPwd, 'confirmPassword': confirm}); + LoadingDialogHelper.hide(); + if (res != null && (res['success'] == true || res['code'] == 200)) { + ToastUtil.showNormal(context, '密码重置成功,请使用新密码登录'); + await _clearUserSession(); + + // 跳转到登录页并清除历史 + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), + (Route route) => false, + ); + } else { + ToastUtil.showNormal(context, res?['message'] ?? '重置密码失败'); + } + } catch (e) { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '重置密码失败: $e'); + } + } + + Future _clearUserSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('isLoggedIn'); + // 如果你有 token 等需要清除,也在这里移除 + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: const MyAppbar(title: '密码找回'), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8), + + // 手机号 + CustomInput.buildInput( + _phoneController, + title: '手机号', + hint: '请输入手机号', + keyboardType: TextInputType.phone, + validator: (v) { + if (v == null || v.isEmpty) return '请输入手机号'; + if (!_isPhoneValid(v.trim())) return '请输入有效手机号'; + return null; + }, + ), + + const SizedBox(height: 12), + + // 验证码行 + Row( + children: [ + Expanded( + child: CustomInput.buildInput( + _codeController, + title: '验证码', + hint: '请输入验证码', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return '请输入验证码'; + return null; + }, + ), + ), + const SizedBox(width: 12), + Column( + children: [ + const SizedBox(height: 40,), + SizedBox( + height: 40, + child: ElevatedButton( + onPressed: _canSend ? _sendVerificationCode : null, + style: ElevatedButton.styleFrom( + backgroundColor: _canSend ? const Color(0xFF2A75F8) : Colors.grey, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 12), + ), + child: Text(_sendText, style: const TextStyle(color: Colors.white)), + ), + ), + ], + ) + ], + ), + + const SizedBox(height: 12), + + // 新密码 + CustomInput.buildInput( + _newPwdController, + title: '新密码', + hint: '请输入新密码', + obscure: _obscureNew, + suffix: IconButton( + icon: Icon(_obscureNew ? Icons.visibility_off : Icons.visibility, color: Colors.grey), + onPressed: () => setState(() => _obscureNew = !_obscureNew), + ), + validator: (v) { + if (v == null || v.isEmpty) return '请输入新密码'; + if (v.length < 8) return '密码长度至少8位'; + return null; + }, + ), + + const SizedBox(height: 12), + + // 确认密码 + CustomInput.buildInput( + _confirmPwdController, + title: '确认新密码', + hint: '请再次输入新密码', + obscure: _obscureConfirm, + suffix: IconButton( + icon: Icon(_obscureConfirm ? Icons.visibility_off : Icons.visibility, color: Colors.grey), + onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), + ), + validator: (v) { + if (v == null || v.isEmpty) return '请确认新密码'; + return null; + }, + ), + + const SizedBox(height: 16), + + Text(textString, style: const TextStyle(color: Colors.red, fontSize: 13)), + const SizedBox(height: 24), + + SizedBox( + width: double.infinity, + height: 46, + child: CustomButton( + onPressed: _handleSubmit, + text: "提交", + backgroundColor: const Color(0xFF2A75F8), + borderRadius: 8, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/mine/mine_feedback_page.dart b/lib/pages/mine/mine_feedback_page.dart new file mode 100644 index 0000000..ad6f2bf --- /dev/null +++ b/lib/pages/mine/mine_feedback_page.dart @@ -0,0 +1,407 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'dart:io'; +import '../../../../../customWidget/photo_picker_row.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +import '../../http/ApiService.dart'; + +// feedback_type.dart +enum FeedbackType { + systemError, + uiOptimization, + designIssue, + performanceIssue, + other, +} + +///问题反馈 + +class FeedbackPage extends StatefulWidget { + const FeedbackPage({super.key}); + + @override + State createState() => _FeedbackPageState(); +} + +class _FeedbackPageState extends State { + final _formKey = GlobalKey(); + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + + // 反馈类型 + FeedbackType? _selectedType = FeedbackType.other; + + // 上传的图片 + List _images = []; + + // 获取反馈类型名称 + String _getTypeName(FeedbackType type) { + switch (type) { + case FeedbackType.systemError: + return '系统错误'; + case FeedbackType.uiOptimization: + return '界面优化'; + case FeedbackType.designIssue: + return '设计缺陷'; + case FeedbackType.performanceIssue: + return '性能问题'; + case FeedbackType.other: + return '其他'; + } + } + + // 获取反馈类型名称 + String _getTypeNum(FeedbackType type) { + switch (type) { + case FeedbackType.systemError: + return '1'; + case FeedbackType.uiOptimization: + return '2'; + case FeedbackType.designIssue: + return '3'; + case FeedbackType.performanceIssue: + return '4'; + case FeedbackType.other: + return '9'; + } + } + + + + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: "问题反馈"), + body: Container( + color: Colors.white, + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题输入 + Row( + children: [ + Text('* ', style: TextStyle(color: Colors.red)), + // 标题 + Text('标题', style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + + const SizedBox(height: 8), + TextFormField( + maxLength: 50, + controller: _titleController, + decoration: const InputDecoration( + hintText: '请输入标题...', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请输入标题'; + } + return null; + }, + ), + const SizedBox(height: 24), + + // 问题描述 + Row( + children: [ + Text('* ', style: TextStyle(color: Colors.red)), + // 标题 + const Text('详细问题和意见', style: TextStyle(fontWeight: FontWeight.bold),), + ], + ), + + const SizedBox(height: 8), + TextFormField( + controller: _descriptionController, + maxLines: 5, + maxLength: 120, + decoration: + const InputDecoration( + hintText: '请补充详细问题和意见...', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.all(10), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return '请补充详细问题和意见'; + } + return null; + }, + ), + const SizedBox(height: 24), + + // 反馈类型 + Row( + children: [ + Text('* ', style: TextStyle(color: Colors.red)), + // 标题 + const Text('反馈类型', style: TextStyle(fontWeight: FontWeight.bold),), + ], + ), + + const SizedBox(height: 8), + Wrap( + spacing: 16, + children: + FeedbackType.values.map((type) { + return ChoiceChip( + label: Text(_getTypeName(type)), + selected: _selectedType == type, + onSelected: (selected) { + setState(() { + if (selected) { + _selectedType = type; + } + }); + }, + ); + }).toList(), + ), + + // const SizedBox(height: 14), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // const Text( + // '请提供相关问题的截图或照片', + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + // Text( + // '${_images.length}/4', + // style: const TextStyle(color: Colors.grey), + // ), + // ],), + + // 图片上传 + const SizedBox(height: 16), + + RepairedPhotoSection( + isRequired:true, + horizontalPadding: 0, + title: "请提供相关问题的截图或照片", + maxCount: 4, + mediaType: MediaType.image, + onChanged: (files) { + // 上传 files 到服务器 + _images.clear(); + for(int i=0;i _removeImage(index), + // child: Container( + // padding: const EdgeInsets.all(2), + // decoration: const BoxDecoration( + // shape: BoxShape.circle, + // color: Colors.black54, + // ), + // child: const Icon( + // Icons.close, + // size: 16, + // color: Colors.white, + // ), + // ), + // ), + // ), + // ], + // ); + // } else { + // return _images.length < 4 + // ? GestureDetector( + // onTap: _pickImage, + // child: Container( + // decoration: BoxDecoration( + // border: Border.all(color: Colors.grey), + // borderRadius: BorderRadius.circular(8), + // ), + // child: const Icon( + // Icons.add, + // size: 40, + // color: Colors.grey, + // ), + // ), + // ) + // : const SizedBox(); + // } + // }, + // ), + const SizedBox(height: 40), + + // 提交按钮 + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _submitFeedback, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '提交', + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + // 提交反馈 + Future _submitFeedback() async { + + final title = _titleController.text.trim(); + final text = _descriptionController.text.trim(); + + if (title.isEmpty ) { + ToastUtil.showNormal(context, '请填写标题'); + // _showMessage('请填写标题'); + return; + } + + if (text.isEmpty ) { + ToastUtil.showNormal(context, '请填写问题和意见'); + // _showMessage('请填写问题和意见'); + return; + } + + if (_images.isEmpty) { + ToastUtil.showNormal(context, '请上传相关图片'); + // _showMessage('请上传隐患图片'); + return; + } + + String imagePaths=""; + for(int i=0;i<_images.length;i++){ + String imagePath=await _reloadFeedBack(_images[i]) ; + + if(0==i){ + imagePaths=imagePath; + }else { + imagePaths = "$imagePaths;$imagePath"; + } + } + + String num=_getTypeNum(_selectedType!); + + _setFeedBack(title,text,imagePaths,num); + + // if (_formKey.currentState!.validate()) { + // // 模拟提交过程 + // showDialog( + // context: context, + // barrierDismissible: false, + // builder: (context) => const Center(child: CircularProgressIndicator()), + // ); + // + // Future.delayed(const Duration(seconds: 2), () { + // Navigator.pop(context); // 关闭加载对话框 + // ScaffoldMessenger.of( + // context, + // ).showSnackBar(const SnackBar(content: Text('反馈提交成功!'))); + // + // // 重置表单 + // _formKey.currentState!.reset(); + // setState(() { + // _images.clear(); + // _selectedType = FeedbackType.other; + // }); + // }); + // } + } + + Future _reloadFeedBack(String imagePath) async { + return ""; + + // try { + // + // final raw = await ApiService.reloadFeedBack(imagePath); + // if (raw['result'] == 'success') { + // return raw['imgPath']; + // }else{ + // // _showMessage('反馈提交失败'); + // return ""; + // } + // + // } catch (e) { + // // 出错时可以 Toast 或者在页面上显示错误状态 + // print('加载首页数据失败:$e'); + // return ""; + // } + } + + Future _setFeedBack(String title, String text, String imagePaths, String num) async { + // try { + // + // final raw = await ApiService.setFeedBack(title,text,num,imagePaths); + // + // if (raw['result'] == 'success') { + // ToastUtil.showNormal(context, '反馈提交成功'); + // Navigator.pop(context); // 关闭加载对话框 + // // _showMessage('反馈提交成功'); + // + // }else{ + // ToastUtil.showNormal(context, '反馈提交失败'); + // // _showMessage('反馈提交失败'); + // } + // + // } catch (e) { + // // 出错时可以 Toast 或者在页面上显示错误状态 + // print('加载首页数据失败:$e'); + // } + } + + void _showMessage(String msg) { + ToastUtil.showNormal(context, msg); + } +} diff --git a/lib/pages/mine/mine_page.dart b/lib/pages/mine/mine_page.dart new file mode 100644 index 0000000..77ddada --- /dev/null +++ b/lib/pages/mine/mine_page.dart @@ -0,0 +1,449 @@ +import 'dart:convert'; + +import 'package:flutter/material.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/pages/home/scan_page.dart'; +import 'package:qhd_prevention/pages/home/userinfo_page.dart'; +import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; +import 'package:qhd_prevention/pages/mine/mine_feedback_page.dart'; +import 'package:qhd_prevention/pages/mine/mine_set_pwd_page.dart'; +import 'package:qhd_prevention/pages/mine/onboarding_full_page.dart'; +import 'package:qhd_prevention/pages/user/full_userinfo_page.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'; + +class MinePage extends StatefulWidget { + const MinePage({super.key}); + + @override + State createState() => MinePageState(); +} + +class MinePageState extends State { + // 设置项状态 + bool notificationsEnabled = false; + bool passwordChanged = false; + bool updateAvailable = false; + bool logoutSelected = false; + bool faceAuthentication = false; + bool scanAuthentication = false; + + String name = '登录/注册'; + String phone = ''; + + void onRouteConfigLoaded() { + if (mounted) { + setState(() { + // _updateMenuVisibility(); + }); + } + } + + @override + void initState() { + // TODO: implement initState + super.initState(); + name = SessionService.instance.name ?? "登录/注册"; + phone = SessionService.instance.phone ?? ""; + } + + @override + @override + Widget build(BuildContext context) { + final double headerHeight = 300.0; + final double overlap = 100.0; + return SizedBox( + height: MediaQuery.of(context).size.height, + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + height: headerHeight, + child: _buildHeaderSection(), + ), + Positioned.fill( + child: NotificationListener( + onNotification: (overscroll) { + overscroll.disallowIndicator(); + return false; + }, + child: ListView( + padding: EdgeInsets.only( + top: headerHeight - overlap, + bottom: 24, + left: 0, + right: 0, + ), + children: [ + _buildSettingsList(), + + SizedBox(height: 15), + + Padding( + padding: EdgeInsetsGeometry.symmetric(horizontal: 15), + child: CustomButton( + text: '退出登录', + textStyle: TextStyle(fontSize: 16), + backgroundColor: Colors.blue, + // borderRadius: 15, + onPressed: () { + CustomAlertDialog.showConfirm( + context, + title: '确认退出', + content: '确定要退出当前账号吗', + onConfirm: () async { + // await AuthService.logout(); // ✅ 等待登出完成 + // if (!mounted) return; + // 清除用户登录状态 + await _clearUserSession(); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (context) => const LoginPage(), + ), + (Route route) => false, + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildHeaderSection() { + return Stack( + alignment: const FractionalOffset(0.5, 0), + children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 0, 0, 10), + child: Image.asset( + "assets/images/my_bg.png", + width: MediaQuery.of(context).size.width, // 获取屏幕宽度 + fit: BoxFit.cover, + ), + ), + + Positioned( + top: 51, + child: Text( + "我的", + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + + // 标语区域 + _buildSloganSection(), + ], + ); + } + + Widget _buildSloganSection() { + return Container( + margin: EdgeInsets.fromLTRB(0, 100, 0, 0), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, // 水平居中 + // crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中(可选) + children: [ + Row( + children: [ + CircleAvatar( + backgroundImage: AssetImage("assets/images/my_bg.png"), + radius: 30, + ), + const SizedBox(width: 16), + Text( + name, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(width: 16), + + ], + ), + ); + } + Widget _buildSettingsList() { + return Container( + margin: const EdgeInsets.fromLTRB(20, 0, 20, 0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + _buildSettingItem( + title: "我的信息", + icon: "assets/images/ico9.png", + value: notificationsEnabled, + num: 0, + onChanged: (value) => setState(() => notificationsEnabled = value!), + ), + _buildSettingItem( + title: "修改密码", + icon: "assets/images/ico16.png", + value: notificationsEnabled, + num: 1, + onChanged: (value) async { + await pushPage(MineSetPwdPage('0'), context); + }, + ), + _buildSettingItem( + title: "扫码入职", + icon: "assets/images/ico10.png", + value: scanAuthentication, + num: 2, + onChanged: (value) async {}, + ), + + _buildSettingItem( + title: "人脸认证", + icon: "assets/images/ico11.png", + value: faceAuthentication, + num: 3, + onChanged: (value) => setState(() => faceAuthentication = value!), + ), + + _buildSettingItem( + title: "证书信息", + icon: "assets/images/ico12.png", + value: passwordChanged, + num: 4, + onChanged: (value) => setState(() => passwordChanged = value!), + ), + + _buildSettingItem( + title: "问题反馈", + icon: "assets/images/ico13.png", + value: passwordChanged, + num: 5, + onChanged: (value) => setState(() => passwordChanged = value!), + ), + + // const Divider(height: 1, indent: 60), + _buildSettingItem( + title: "版本更新", + icon: "assets/images/ico14.png", + value: updateAvailable, + num: 6, + onChanged: (value) => setState(() => updateAvailable = value!), + ), + + _buildSettingItem( + title: "关于我们", + icon: "assets/images/ico15.png", + value: logoutSelected, + num: 7, + onChanged: (value) { + setState(() => logoutSelected = value!); + // if (value == true) { + // _showLogoutConfirmation(); + // } + }, + ), + _buildSettingItem( + title: "账户注销", + icon: "assets/images/ico15.png", + value: logoutSelected, + num: 8, + onChanged: (value) { + + }, + ), + ], + ), + ); + } + + Widget _buildSettingItem({ + required String title, + required String icon, + required bool value, + required int num, + required ValueChanged onChanged, + }) { + return GestureDetector( + onTap: () async { + switch (num) { + case 0: + pushPage(FullUserinfoPage(isEidt: false, isChooseFirm: true,), context); + break; + case 1: + await pushPage(MineSetPwdPage('0'), context); + + break; + case 2: + final result = await pushPage( + ScanPage(type: ScanType.Onboarding), + context, + ); + if (result == null) { + return; + } + pushPage(OnboardingFullPage(scanData: result), context); + + break; + + case 3: + pushPage( + const FaceRecognitionPage( + studentId: '', + data: {}, + mode: FaceMode.setUpdata, + ), + context, + ); + // pushPage(ChangePassPage(), context); + break; + case 4: + // _showLogoutConfirmation(); + break; + case 5: + pushPage(FeedbackPage(), context); + break; + case 6: + break; + case 7: + break; + } + }, + child: ListTile( + leading: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Image.asset(icon, fit: BoxFit.cover), + ), + title: Text( + title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + + trailing: Transform.scale( + scale: 1.2, + child: Icon(Icons.chevron_right), + // Image.asset( + // "assets/images/right.png", + // fit: BoxFit.cover, + // width: 15, + // height: 15, + // ), + ), + ), + ); + } + + Widget _buildActionButton() { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + // 保存设置逻辑 + _saveSettings(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF1A237E), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 3, // 注意:elevation应该放在styleFrom内部 + ), + child: const Text( + "保存设置", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + ), + ); + } + + void _saveSettings() { + // 这里可以添加保存设置到服务器的逻辑 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text("设置已保存成功"), + backgroundColor: Colors.green[700], + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ); + } + + Future _clearUserSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('isLoggedIn'); // 清除登录状态 + } +} + +class _StatItem extends StatelessWidget { + final String number; + final String label; + + const _StatItem({required this.number, required this.label}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // 数字圆圈 + Center( + child: Text( + number, + style: const TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + + const SizedBox(height: 8), + + // 标签 + Text( + label, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/lib/pages/mine/mine_set_pwd_page.dart b/lib/pages/mine/mine_set_pwd_page.dart new file mode 100644 index 0000000..dffa1b5 --- /dev/null +++ b/lib/pages/mine/mine_set_pwd_page.dart @@ -0,0 +1,278 @@ +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/pages/my_appbar.dart'; +import 'package:qhd_prevention/pages/user/login_page.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../http/ApiService.dart'; + +class MineSetPwdPage extends StatefulWidget { + const MineSetPwdPage(this.type, {super.key}); + + final String type; + @override + State createState() => _MineSetPwdPageState(); +} + +class _MineSetPwdPageState extends State { + final _formKey = GlobalKey(); + + final _oldPwdController = TextEditingController(); + final _newPwdController = TextEditingController(); + final _confirmPwdController = TextEditingController(); + + bool _obscureOld = true; + bool _obscureNew = true; + bool _obscureConfirm = true; + + String textString = + "为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + + Map passData = { + "id": '', + "password": "", + "newPassword": "", + }; + + @override + void initState() { + super.initState(); + switch (widget.type) { + case "0": + textString = + "密码长度8-18位,需包含数字、字母、英文符号至少2种或以上元素"; + break; + case "1": + textString = + "检测到您的密码为弱密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "2": + textString = + "检测到您30天内未修改密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "3": + textString = + "检测到您的密码为弱密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + case "4": + textString = + "检测到您30天内未修改密码,请修改密码后重新登录。为了您的账户安全,请确保密码长度为 8-18 位,必须包含大小写字母+数字+特殊字符,例如:Aa@123456"; + break; + } + } + + @override + void dispose() { + _oldPwdController.dispose(); + _newPwdController.dispose(); + _confirmPwdController.dispose(); + super.dispose(); + } + + // 与登录页一致的输入框样式 + Widget _buildRoundedInput({ + required TextEditingController controller, + required String hint, + bool obscure = false, + VoidCallback? toggleObscure, + FormFieldValidator? validator, + }) { + return TextFormField( + controller: controller, + obscureText: obscure, + validator: validator, + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: Colors.grey), + filled: true, + fillColor: const Color(0xFFE2EBF4), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + suffixIcon: toggleObscure == null + ? null + : IconButton( + icon: Icon( + obscure ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + ), + onPressed: toggleObscure, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + ), + style: const TextStyle(color: Colors.black), + ); + } + + // 密码复杂度校验 + bool isPasswordValid(String password) { + final hasUpperCase = RegExp(r'[A-Z]'); + final hasLowerCase = RegExp(r'[a-z]'); + final hasNumber = RegExp(r'[0-9]'); + final hasSpecialChar = RegExp(r'[!@#\$%\^&\*\(\)_\+\-=\[\]\{\};:"\\|,.<>\/\?~`]'); + return hasUpperCase.hasMatch(password) && + hasLowerCase.hasMatch(password) && + hasNumber.hasMatch(password) && + hasSpecialChar.hasMatch(password); + } + + void _handleSubmit() async { + // 使用 Form 验证必填 + if (!(_formKey.currentState?.validate() ?? false)) return; + + final oldPwd = _oldPwdController.text.trim(); + final newPwd = _newPwdController.text.trim(); + final confirmPwd = _confirmPwdController.text.trim(); + + if (newPwd != confirmPwd) { + ToastUtil.showNormal(context, '新密码和确认密码两次输入的密码不一致'); + return; + } + + if (newPwd.length < 8) { + ToastUtil.showNormal(context, '新密码需要大于8位'); + return; + } + + if (newPwd.length > 32) { + ToastUtil.showNormal(context, '新密码需要小于32位'); + return; + } + + if (!isPasswordValid(newPwd)) { + ToastUtil.showNormal(context, '新密码必须包含大小写字母、数字和特殊符号。'); + return; + } + + await _changePass(oldPwd, newPwd, confirmPwd); + } + + Future _changePass(String oldPwd, String newPwd, String confirmPwd) async { + try { + passData['id'] = SessionService.instance.accountId ?? ""; + passData['password'] = oldPwd; + passData['newPassword'] = newPwd; + passData['confirmPassword'] = confirmPwd; + + final raw = await AuthApi.changePassWord(passData); + + if (raw['success'] == true) { + ToastUtil.showNormal(context, '新密码修改成功!'); + Navigator.pop(context, true); + + // 清除用户登录状态 + await _clearUserSession(); + + // 跳转到登录页并清除所有历史路由 + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), + (Route route) => false, + ); + } else if (raw['success'] == false) { + ToastUtil.showNormal(context, '当前密码密码有误'); + } else { + ToastUtil.showNormal(context, '登录错误!请联系管理员'); + } + } catch (e) { + print('修改密码出错:$e'); + ToastUtil.showNormal(context, '登录错误!请联系管理员'); + } + } + + Future _clearUserSession() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('isLoggedIn'); // 清除登录状态 + // 根据你项目的实际 key 再删除 token 等(如果有) + // await prefs.remove('user_token'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: const MyAppbar(title: '修改密码'), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '修改密码', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 30), + + // 当前密码 + _buildRoundedInput( + controller: _oldPwdController, + hint: '当前密码', + obscure: _obscureOld, + toggleObscure: () => setState(() => _obscureOld = !_obscureOld), + validator: (v) { + if (v == null || v.isEmpty) return '请输入当前密码'; + return null; + }, + ), + const SizedBox(height: 20), + + // 新密码 + _buildRoundedInput( + controller: _newPwdController, + hint: '新密码', + obscure: _obscureNew, + toggleObscure: () => setState(() => _obscureNew = !_obscureNew), + validator: (v) { + if (v == null || v.isEmpty) return '请输入新密码'; + return null; + }, + ), + const SizedBox(height: 20), + + // 确认新密码 + _buildRoundedInput( + controller: _confirmPwdController, + hint: '确认新密码', + obscure: _obscureConfirm, + toggleObscure: () => setState(() => _obscureConfirm = !_obscureConfirm), + validator: (v) { + if (v == null || v.isEmpty) return '请输入确认密码'; + return null; + }, + ), + const SizedBox(height: 15), + + Text( + textString, + style: const TextStyle(color: Colors.red, fontSize: 13), + ), + const SizedBox(height: 30), + + SizedBox( + width: double.infinity, + height: 45, + child: CustomButton( + onPressed: _handleSubmit, + text: "提交", + backgroundColor: Colors.blue, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/mine/mine_sign_page.dart b/lib/pages/mine/mine_sign_page.dart new file mode 100644 index 0000000..76856b0 --- /dev/null +++ b/lib/pages/mine/mine_sign_page.dart @@ -0,0 +1,365 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'dart:ui' as ui; +import 'package:flutter/services.dart'; + +import 'package:path_provider/path_provider.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class MineSignPage extends StatelessWidget { + const MineSignPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const SignatureConfirmPage(), + ); + } +} + +class SignatureConfirmPage extends StatefulWidget { + const SignatureConfirmPage({super.key}); + + @override + State createState() => _SignatureConfirmPageState(); +} + +class _SignatureConfirmPageState extends State { + final GlobalKey _signatureKey = GlobalKey(); + List _points = []; + bool _hasSignature = false; + File? fileN; + Uint8List? _postBytes; + late String imagepath = ""; + + void _clearSignature() { + setState(() { + _points.clear(); + _hasSignature = false; + }); + } + + void _confirmSignature() { + if (!_hasSignature) { + ToastUtil.showNormal(context, '请先签名'); + return; + } + _saveSignPic(); + + // // 保存签名逻辑 + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('签名已确认')), + // ); + //模拟保存后返回 + } + + // 保存签名 + void _saveSignPic() async { + LoadingDialogHelper.show(); + RenderRepaintBoundary boundary = _signatureKey.currentContext! + .findRenderObject() as RenderRepaintBoundary; + var image = await boundary.toImage(pixelRatio: 1); + ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + + int timestamp = DateTime.now().millisecondsSinceEpoch; + + Directory dir = await getTemporaryDirectory(); + // 在文件名中添加时间戳 + String path = '${dir.path}/sign_$timestamp.png'; + + File file2 = File(path); + // 检查文件是否存在 + if (await file2.exists()) { + await file2.delete(); + } + + var file = await File(path).create(recursive: true); + if (byteData != null) { + file.writeAsBytesSync(byteData.buffer.asInt8List(), flush: true); + setState(() { + _postBytes = byteData.buffer.asUint8List(); + fileN = file; + imagepath = file.path; + Future.delayed(const Duration(milliseconds: 500), () { + LoadingDialogHelper.hide(); + Navigator.pop(context, imagepath); + }); + }); + } + } + + @override + void initState() { + super.initState(); + NativeOrientation.setLandscape(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeRight, + DeviceOrientation.landscapeLeft, + ]); + } + + @override + void dispose() { + // 不要忘记重置方向设置,以避免影响其他页面或应用。 + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + NativeOrientation.setPortrait(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + // 标题区域 + _buildTitleBar(), + // MyAppbar(title: "签字"), + // 签字区域 + Expanded( + child: _buildSignatureArea(), + ), + + // 按钮区域 + _buildActionButtons(), + ], + ), + ), + ); + } + + Widget _buildTitleBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + const Text( + '签字', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 48), // 占位保持标题居中 + ], + ), + ); + } + + Widget _buildSignatureArea() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + children: [ + + // 签字画布 + Expanded( + child: GestureDetector( + onPanStart: (details) { + setState(() { + _points.add(details.localPosition); + _hasSignature = true; + }); + }, + onPanUpdate: (details) { + setState(() { + _points.add(details.localPosition); + }); + }, + onPanEnd: (details) { + setState(() { + _points.add(null); + }); + }, + child: RepaintBoundary( + key: _signatureKey, + child: Container( + // 给画布添加白色背景,不透明导出 + color: Colors.white, + child: Stack( + children: [ + // if (imagepath.length > 0) + // Image.file( + // File(imagepath), // 显示选择的图片文件 + // fit: BoxFit.contain, // 设置图片填充方式为完整显示,保持宽高比例 + // ), + + // 背景横线 + _buildBackgroundLines(), + + // 签名画布 + CustomPaint( + painter: SignaturePainter(points: _points), + size: Size.infinite, + ), + + // 提示文字 + if (!_hasSignature) + const Center( + child: Text( + '请在此处签名', + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBackgroundLines() { + return LayoutBuilder( + builder: (context, constraints) { + return CustomPaint( + painter: BackgroundLinesPainter(), + size: Size(constraints.maxWidth, constraints.maxHeight), + ); + }, + ); + } + + Widget _buildActionButtons() { + return Padding( + padding: const EdgeInsets.only(left: 24,right: 24,top: 10,bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 重签按钮 + Expanded( + child: OutlinedButton( + onPressed: _clearSignature, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 8), + side: const BorderSide(color: Colors.blue), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '重签', + style: TextStyle( + fontSize: 16, + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + const SizedBox(width: 20), + + // 确定按钮 + Expanded( + child: ElevatedButton( + onPressed: _confirmSignature, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + padding: const EdgeInsets.symmetric(vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '确定', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } +} + +// 签名绘制器 - 修改后的版本 +class SignaturePainter extends CustomPainter { + final List points; + + SignaturePainter({required this.points}); + + @override + void paint(Canvas canvas, Size size) { + // 创建裁剪区域,确保只在画布范围内绘制 + canvas.save(); + canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height)); + + Paint paint = Paint() + ..color = Colors.black + ..strokeCap = StrokeCap.round + ..strokeWidth = 3.0; + + for (int i = 0; i < points.length - 1; i++) { + if (points[i] != null && points[i + 1] != null) { + // 确保点都在画布范围内 + Offset start = Offset( + points[i]!.dx.clamp(0, size.width), + points[i]!.dy.clamp(0, size.height), + ); + Offset end = Offset( + points[i + 1]!.dx.clamp(0, size.width), + points[i + 1]!.dy.clamp(0, size.height), + ); + canvas.drawLine(start, end, paint); + } + } + + canvas.restore(); + } + + @override + bool shouldRepaint(SignaturePainter oldDelegate) => + oldDelegate.points != points; +} + +// 背景横线绘制器 +class BackgroundLinesPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint() + // ..color = Colors.grey[200]! + ..color = Color.from(alpha: 0, red: 0, green: 0, blue: 0)! + ..strokeWidth = 0; + + // 绘制横线 + for (double y = 40; y < size.height; y += 40) { + canvas.drawLine( + Offset(0, y), + Offset(size.width, y), + paint, + ); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; +} \ No newline at end of file diff --git a/lib/pages/mine/onboarding_full_page.dart b/lib/pages/mine/onboarding_full_page.dart new file mode 100644 index 0000000..359adb7 --- /dev/null +++ b/lib/pages/mine/onboarding_full_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/bottom_picker.dart'; +import 'package:qhd_prevention/customWidget/custom_button.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'; +import 'package:qhd_prevention/pages/main_tab.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/services/auth_service.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class OnboardingFullPage extends StatefulWidget { + const OnboardingFullPage({super.key, required this.scanData}); + final Map scanData; + @override + State createState() => _OnboardingFullPageState(); +} + +class _OnboardingFullPageState extends State { + Map pd = {}; + // 部门列表 + List _deptList = []; + @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) { + _deptList = result['data']; + } + } catch (e) { + } + } + // 提交 + Future _saveSuccess() async { + if (!FormUtils.hasValue(pd, 'corpinfoId')) { + ToastUtil.showNormal(context, '请选择部门'); + return; + } + if (!FormUtils.hasValue(pd, 'postName')) { + ToastUtil.showNormal(context, '请输入岗位'); + return; + } + LoadingDialogHelper.show(); + pd['id'] = widget.scanData['id']; + try { + final result = await BasicInfoApi.userFirmEntry(pd); + LoadingDialogHelper.hide(); + if (result['success'] == true) { + ToastUtil.showNormal(context, '操作成功'); + _relogin(); + } + } catch (e) { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '操作成功'); + } + } + /// 重新登录 + Future _relogin() async { + final prefs = await SharedPreferences.getInstance(); + final username = prefs.getString('savePhone') ?? ''; + final password = prefs.getString('savePass') ?? ''; + + try { + Map data = { + 'id': widget.scanData['id'] ?? '', + 'corpinfoId':widget.scanData['corpinfoId'] ?? '', + }; + final result = await AuthService.gbsLogin(username, password, data); + if (result['success'] == true) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const MainPage(isChooseFirm: true,)), + ); + } + } catch (e) { + ToastUtil.showNormal(context, '重新登录失败'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: '信息补充'), + body: SafeArea( + child: ItemListWidget.itemContainer( + horizontal: 5, + ListView( + children: [ + ItemListWidget.selectableLineTitleTextRightButton( + verticalInset: 15, + label: '选择入职部门:', + 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']; + + }); + } + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '岗位(工种):', + isRequired: true, + hintText: '请输入姓名', + text: "", + isEditable: true, + onChanged: (value) { + pd['postName'] = value; + }, + ), + const Divider(), + const SizedBox(height: 20), + CustomButton( + text: '保存', + backgroundColor: Colors.blue, + onPressed: () { + _saveSuccess(); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/mine/webViewPage.dart b/lib/pages/mine/webViewPage.dart new file mode 100644 index 0000000..8aa4514 --- /dev/null +++ b/lib/pages/mine/webViewPage.dart @@ -0,0 +1,68 @@ + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + + +class WebViewPage extends StatefulWidget { + final String url; + final String name; + + const WebViewPage({Key? key, required this.url, required this.name,}) : super(key: key); + + @override + State createState() => _WebViewPageState(name); + + +} + +class _WebViewPageState extends State { + late final WebViewController _controller; + final String name; + ValueNotifier loadingProgress = ValueNotifier(0.0); + ValueNotifier isLoading = ValueNotifier(true); + + _WebViewPageState(this.name); + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(NavigationDelegate( + onProgress: (progress) { + loadingProgress.value = progress / 100; + if (progress == 100) isLoading.value = false; + }, + onPageStarted: (url) => isLoading.value = true, + onPageFinished: (url) => isLoading.value = false, + )) + ..loadRequest(Uri.parse(widget.url)); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + MyAppbar(title: name,onBackPressed: () async { + if (await _controller.canGoBack()) { + _controller.goBack(); + } else{ + Navigator.of(context).pop(); + } + },), + Expanded( child: WebViewWidget(controller: _controller),), + // ValueListenableBuilder( + // valueListenable: isLoading, + // builder: (context, loading, _) { + // return loading + // ? const Center(child: CircularProgressIndicator()) + // : const SizedBox(); + // }, + // ), + ], + + ); + + } +} \ No newline at end of file diff --git a/lib/pages/my_appbar.dart b/lib/pages/my_appbar.dart new file mode 100644 index 0000000..06b00ce --- /dev/null +++ b/lib/pages/my_appbar.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'dart:io' show Platform; + +class MyAppbar extends StatelessWidget implements PreferredSizeWidget { + final String title; + final VoidCallback? onBackPressed; + final Color backgroundColor; + final Color textColor; + final List? actions; + final bool isBack; + final bool centerTitle; // 新增:控制标题是否居中 + + const MyAppbar({ + Key? key, + required this.title, + this.onBackPressed, + this.backgroundColor = const Color(0xFF1C61FF), + this.textColor = Colors.white, + this.actions, + this.isBack = true, + this.centerTitle = true, // 默认居中 + }) : super(key: key); + + // 根据平台设置不同高度 + @override + Size get preferredSize { + // iOS使用更紧凑的高度(44点),Android保持默认(56点) + return Size.fromHeight(Platform.isIOS ? 44.0 : kToolbarHeight); + } + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: backgroundColor, + automaticallyImplyLeading: false, + centerTitle: centerTitle, + toolbarHeight: preferredSize.height, // 使用计算的高度 + title: Text( + title, + style: TextStyle( + color: textColor, + fontSize: Platform.isIOS ? 17.0 : 18.0, // iOS使用更小字号 + fontWeight: FontWeight.w600, // iOS使用中等字重 + ), + ), + leading: isBack ? _buildBackButton(context) : null, + actions: _buildActions(), + elevation: Platform.isIOS ? 0 : 4, // iOS无阴影 + // iOS添加底部边框 + shape: Platform.isIOS + ? const Border(bottom: BorderSide(color: Colors.black12, width: 0.5)) + : null, + ); + } + + // 返回按钮 + Widget _buildBackButton(BuildContext context) { + return Padding( + padding: EdgeInsets.only(left: Platform.isIOS ? 8.0 : 16.0), + child: IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, + color: textColor, + size: Platform.isIOS ? 20.0 : 24.0, // iOS使用更小图标 + ), + padding: EdgeInsets.zero, // 移除默认内边距 + constraints: const BoxConstraints(), // 移除默认约束 + onPressed: onBackPressed ?? () => Navigator.of(context).pop(), + ), + ); + } + + // 右侧按钮间距 + List? _buildActions() { + if (actions == null) return null; + + return [ + Padding( + padding: EdgeInsets.only(right: Platform.isIOS ? 8.0 : 16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: actions!, + ), + ) + ]; + } +} \ No newline at end of file diff --git a/lib/pages/user/CustomInput.dart b/lib/pages/user/CustomInput.dart new file mode 100644 index 0000000..a619cf1 --- /dev/null +++ b/lib/pages/user/CustomInput.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; + +class CustomInput { + static Widget buildInput( + TextEditingController? controller, { + String? title, + String? hint, + bool obscure = false, + Widget? suffix, + TextInputType? keyboardType, + FocusNode? focusNode, + FormFieldValidator? validator, + ValueChanged? onChanged, + String? initialValue, + + bool labelInline = false, + double? labelWidth, + }) { + return _CustomInputWidget( + externalController: controller, + title: title, + hint: hint, + obscure: obscure, + suffix: suffix, + keyboardType: keyboardType, + focusNode: focusNode, + validator: validator, + onChanged: onChanged, + initialValue: initialValue, + labelInline: labelInline, + labelWidth: labelWidth, + ); + } +} + +class _CustomInputWidget extends StatefulWidget { + const _CustomInputWidget({ + Key? key, + this.externalController, + this.title, + this.hint, + this.obscure = false, + this.suffix, + this.keyboardType, + this.focusNode, + this.validator, + this.onChanged, + this.initialValue, + this.labelInline = false, + this.labelWidth, + }) : super(key: key); + + final TextEditingController? externalController; + final String? title; + final String? hint; + final bool obscure; + final Widget? suffix; + final TextInputType? keyboardType; + final FocusNode? focusNode; + final FormFieldValidator? validator; + final ValueChanged? onChanged; + final String? initialValue; + + // new + final bool labelInline; + final double? labelWidth; + + @override + State<_CustomInputWidget> createState() => _CustomInputWidgetState(); +} + +class _CustomInputWidgetState extends State<_CustomInputWidget> { + late final TextEditingController _controller; + late final bool _controllerIsExternal; + bool _showClear = false; + + @override + void initState() { + super.initState(); + _controllerIsExternal = widget.externalController != null; + _controller = widget.externalController ?? TextEditingController(text: widget.initialValue ?? ''); + _showClear = _controller.text.isNotEmpty; + _controller.addListener(_onTextChange); + } + + void _onTextChange() { + final has = _controller.text.isNotEmpty; + if (has != _showClear) { + setState(() { + _showClear = has; + }); + } + if (widget.onChanged != null) widget.onChanged!(_controller.text); + } + + @override + void dispose() { + _controller.removeListener(_onTextChange); + if (!_controllerIsExternal) { + _controller.dispose(); + } + super.dispose(); + } + + void _clearText() { + _controller.clear(); + // listener will trigger onChanged + } + + Widget _buildTextField({EdgeInsetsGeometry? contentPadding}) { + return SizedBox( + height: 40, + child: TextFormField( + controller: _controller, + obscureText: widget.obscure, + keyboardType: widget.keyboardType, + validator: widget.validator, + focusNode: widget.focusNode, + // 保证文字垂直居中 + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: widget.hint, + hintStyle: const TextStyle(color: Color(0xFFB6BAC9), fontSize: 16), + suffixIcon: widget.suffix ?? (_showClear + ? IconButton( + icon: const Icon(Icons.cancel, size: 20, color: Colors.grey), + onPressed: _clearText, + ) + : null), + isDense: true, + contentPadding: contentPadding ?? const EdgeInsets.symmetric(horizontal: 0, vertical: 10), + border: InputBorder.none, + ), + style: const TextStyle(color: Colors.black87, fontSize: 16), + ), + ); + } + + @override + Widget build(BuildContext context) { + final hasTitle = (widget.title ?? '').isNotEmpty; + if (widget.labelInline && hasTitle) { + final double labelW = widget.labelWidth ?? 100.0; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: labelW, + child: Text( + widget.title!, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + ), + Expanded( + child: Container( + + child: _buildTextField(contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10)), + ), + ), + ], + ), + const SizedBox(height: 10), + const Divider(height: 0.5, thickness: 1, color: Color(0xFFBDBDBD)), + ], + ); + } + + // 默认竖排(标题在上) + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasTitle) + Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Text( + widget.title!, + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500), + ), + ), + _buildTextField(), + const SizedBox(height: 10), + const Divider(height: 0.5, thickness: 1, color: Color(0xFFBDBDBD)), + ], + ); + } +} diff --git a/lib/pages/user/choose_userFirm_page.dart b/lib/pages/user/choose_userFirm_page.dart new file mode 100644 index 0000000..fe41e03 --- /dev/null +++ b/lib/pages/user/choose_userFirm_page.dart @@ -0,0 +1,194 @@ + +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/pages/main_tab.dart'; +import 'package:qhd_prevention/services/StorageService.dart'; +import 'package:qhd_prevention/services/auth_service.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + + +class ChooseUserfirmPage extends StatefulWidget { + const ChooseUserfirmPage({super.key, required this.firms, required this.userName, required this.password}); + final List firms; + final String userName; + final String password; + + @override + _ChooseUserfirmPageState createState() => _ChooseUserfirmPageState(); +} + +class _ChooseUserfirmPageState extends State { + int _selectedIndex = -1; + + String _displayLabel(Map m) { + final keys = ['corpName']; + for (final k in keys) { + if (m.containsKey(k) && m[k] != null) return m[k].toString(); + } +// 如果都没有,则找到第一个 value 为字符串或数字的字段 + for (final entry in m.entries) { + final v = entry.value; + if (v is String || v is num) return v.toString(); + } + return '未命名企业'; + } + + @override + void initState() { + super.initState(); + + } + + @override + void dispose() { + + super.dispose(); + } + Future _login() async { + if (_selectedIndex < 0) { + ToastUtil.showNormal(context, '请选择已入职的相关方单位'); + return; + } + final params = { + 'corpId': widget.firms[_selectedIndex]['id'], + 'corpName': widget.firms[_selectedIndex]['corpName'], + }; + LoadingDialogHelper.show(); + final result = await AuthService.gbsLogin(widget.userName, widget.password, params); + LoadingDialogHelper.hide(); + if (result['success'] == true) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const MainPage(isChooseFirm: true,)), + ); + } + + // final result = AuthService.gbsLogin(username, password, params) + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + double height = 230.0; + return Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: true, + body: Stack( + children: [ + // 背景图:铺满屏幕 + Positioned( + left: 0, + top: 0, + right: 0, + child: Image.asset( + 'assets/images/loginbg.png', + fit: BoxFit.fitWidth, + ), + ), + Positioned( + top: 0, + left: 20, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 70), // 顶部间距 + Image.asset( + "assets/images/logo.png", + width: 40, + height: 40, + ), + const SizedBox(height: 10), + const Text( + "欢迎使用", + style: TextStyle( + color: Colors.white, + fontSize: 23, + fontWeight: FontWeight.w500, + ), + ), + const Text( + "秦港-相关方安全管理平台", + style: TextStyle( + color: Colors.white, + fontSize: 23, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + Positioned( + top: height, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), + child:Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // const SizedBox(width: 20,), + Text( + '请选择已入职的相关方单位', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 20,), + ], + ), + ) + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + top: height + 50, + child:Container( + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 0), + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 0), + + itemCount: widget.firms.length + 1, + separatorBuilder: (_, __) => const SizedBox(height: 1,), + itemBuilder: (context, index) { + if (index == widget.firms.length) { + return CustomButton(text: '登录', + // padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 100), + height: 40, + onPressed: () { + _login(); + } + ); + } + final firm = widget.firms[index]; + return RadioListTile( + + value: index, + activeColor: Colors.blue, + groupValue: _selectedIndex, + onChanged: (v) { + setState(() { + _selectedIndex = v!; + }); + }, + title: Text(_displayLabel(firm)), + controlAffinity: ListTileControlAffinity.leading, + ); + }, + ), + ) + ), + + ], + ), + ); + } + +} diff --git a/lib/pages/user/firm_list_page.dart b/lib/pages/user/firm_list_page.dart new file mode 100644 index 0000000..d164f93 --- /dev/null +++ b/lib/pages/user/firm_list_page.dart @@ -0,0 +1,465 @@ +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/search_bar_widget.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/pages/mine/onboarding_full_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; + +// lpinyin 用于中文转拼音 +import 'package:lpinyin/lpinyin.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class FirmListPage extends StatefulWidget { + const FirmListPage({super.key}); + + @override + State createState() => _FirmListPageState(); +} + +class _FirmListPageState extends State { + List _firmList = []; // 原始数据(全部) + List _displayList = []; // 经过搜索过滤后用于显示的数据 + + final Map> _sections = {}; + final List _sectionOrder = []; + + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + + static const double headerHeight = 40.0; + static const double itemHeight = 64.0; + + @override + void initState() { + super.initState(); + _getFirmList(); + } + + Future _getFirmList() async { + try { + LoadingDialogHelper.show(); + final result = await BasicInfoApi.getFirmList({'enterpriseType': 3}); + LoadingDialogHelper.hide(); + if (result['success'] == true && result['data'] is List) { + setState(() { + _firmList = List.from(result['data']); + _displayList = List.from(_firmList); + }); + _groupAndSort(); + } else { + setState(() { + _firmList = []; + _displayList = []; + _sections.clear(); + _sectionOrder.clear(); + }); + } + } catch (e) { + // 处理异常 + setState(() { + _firmList = []; + _displayList = []; + _sections.clear(); + _sectionOrder.clear(); + }); + } + } + + // 取企业显示名(优先字段) + String _firmName(Map m) { + final keys = [ + 'corpName', + 'companyName', + 'name', + 'firmName', + 'title', + 'epcProjectName', + ]; + for (final k in keys) { + if (m.containsKey(k) && + m[k] != null && + m[k].toString().trim().isNotEmpty) { + return m[k].toString().trim(); + } + } + // 若没有合适字段,尝试取第一个非空 value + for (final entry in m.entries) { + final v = entry.value; + if (v is String && v.trim().isNotEmpty) return v.trim(); + if (v is num) return v.toString(); + } + return '未命名企业'; + } + + // 将 displayList 按拼音首字母分组并排序 + void _groupAndSort() { + _sections.clear(); + + for (final raw in _displayList) { + if (raw is! Map) continue; + final name = _firmName(raw); + + String shortPinyin = ''; + try { + shortPinyin = PinyinHelper.getShortPinyin(name); + } catch (_) { + shortPinyin = ''; + } + + final firstLetter = + (shortPinyin.isNotEmpty + ? shortPinyin[0].toUpperCase() + : (name.isNotEmpty ? name[0].toUpperCase() : '#')); + final letter = + RegExp(r'^[A-Z]$').hasMatch(firstLetter) ? firstLetter : '#'; + + _sections.putIfAbsent(letter, () => []).add(raw); + } + + // 对每个分组内部按拼音全拼排序(不带声调) + for (final k in _sections.keys) { + _sections[k]!.sort((a, b) { + final na = _firmName(a); + final nb = _firmName(b); + + String pa; + String pb; + try { + pa = + PinyinHelper.getPinyin( + na, + separator: '', + format: PinyinFormat.WITHOUT_TONE, + ).toLowerCase(); + } catch (_) { + pa = na.toLowerCase(); + } + try { + pb = + PinyinHelper.getPinyin( + nb, + separator: '', + format: PinyinFormat.WITHOUT_TONE, + ).toLowerCase(); + } catch (_) { + pb = nb.toLowerCase(); + } + return pa.compareTo(pb); + }); + } + + // 构建分组顺序:A..Z,然后 '#' + final letters = List.generate( + 26, + (i) => String.fromCharCode(65 + i), + ); + final order = []; + for (final l in letters) { + if (_sections.containsKey(l)) order.add(l); + } + if (_sections.containsKey('#')) order.add('#'); + + setState(() { + _sectionOrder + ..clear() + ..addAll(order); + }); + } + + // 过滤函数:支持中文直接匹配,也支持拼音匹配 + void _applyFilter(String q) { + final query = q.trim(); + if (query.isEmpty) { + setState(() { + _displayList = List.from(_firmList); + }); + _groupAndSort(); + return; + } + + final qLower = query.toLowerCase(); + final filtered = []; + + for (final raw in _firmList) { + if (raw is! Map) continue; + final name = _firmName(raw); + final nameLower = name.toLowerCase(); + + bool matched = false; + // 1) 中文/英文直接包含 + if (nameLower.contains(qLower)) matched = true; + + // 2) 拼音匹配 + if (!matched) { + try { + final pinyin = + PinyinHelper.getPinyin( + name, + separator: '', + format: PinyinFormat.WITHOUT_TONE, + ).toLowerCase(); + if (pinyin.contains(qLower)) matched = true; + } catch (_) { + // ignore + } + } + + if (matched) filtered.add(raw); + } + + setState(() { + _displayList = filtered; + }); + _groupAndSort(); + } + + // 计算某个分组的列表起始偏移(基于 headerHeight/itemHeight 的估算) + double _offsetForSection(String letter) { + double offset = 0.0; + for (final l in _sectionOrder) { + if (l == letter) break; + offset += headerHeight; + final cnt = _sections[l]?.length ?? 0; + offset += cnt * itemHeight; + } + return offset; + } + + void _jumpToLetter(String letter) { + if (_sectionOrder.isEmpty) return; + + if (_sections.containsKey(letter)) { + final off = _offsetForSection(letter); + _scrollController.animateTo( + off, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + return; + } + + final all = + List.generate(26, (i) => String.fromCharCode(65 + i)) + ['#']; + final idx = all.indexOf(letter); + if (idx == -1) return; + for (int i = idx + 1; i < all.length; i++) { + final l = all[i]; + if (_sections.containsKey(l)) { + final off = _offsetForSection(l); + _scrollController.animateTo( + off, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + return; + } + } + + final last = _sectionOrder.last; + final off = _offsetForSection(last); + _scrollController.animateTo( + off, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + } + + List get _azIndex { + final list = List.generate(26, (i) => String.fromCharCode(65 + i)); + list.add('#'); + return list; + } + + @override + Widget build(BuildContext context) { + // 右侧字母索引(灰色圆角背景),使用固定高度以忽略键盘导致的可用高度变化 + final mq = MediaQuery.of(context); + final fixedIndexHeight = + mq.size.height - kToolbarHeight - mq.padding.top - 24-100; + return Scaffold( + backgroundColor: Colors.white, + + appBar: MyAppbar(title: '选择企业'), + body: SafeArea( + child: Column( + children: [ + // 搜索框(固定在顶部) + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: SearchBarWidget( + onSearch: (keyboard) {}, + controller: _searchController, + isShowSearchButton: false, + resetButtonText: '重置', + showResetButton: true, + onTextChanged: (text) { + _applyFilter(text); + }, + onReset: () { + _searchController.clear(); + _applyFilter(''); + }, + ), + ), + + // 列表区域 + Expanded( + child: Stack( + children: [ + // 列表区域:RefreshIndicator 必须包裹可滚动控件(ListView) + Padding( + padding: const EdgeInsets.only(right: 48.0), // 给右侧字母索引留空间 + child: _buildListView(), + ), + + // 24 是上/下 margin 的大致预留(12 + 12),根据你视觉需要可调 + Positioned( + right: 8, + top: 12, + // 不使用 bottom(避免随键盘收缩),改为固定高度(独立于 viewInsets) + height: fixedIndexHeight, + child: _buildAlphabetIndex(fixedIndexHeight), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildListView() { + if (_sectionOrder.isEmpty) { + return ListView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + children: [ + const SizedBox(height: 30), + Center( + child: Text( + _displayList.isEmpty ? ' 暂无企业' : ' 正在加载…', + style: TextStyle(fontSize: 15, color: Colors.grey[600]), + ), + ), + ], + ); + } + + final children = []; + for (final letter in _sectionOrder) { + children.add(_buildSectionHeader(letter)); + final items = _sections[letter] ?? []; + for (final item in items) { + children.add(_buildItem(item)); + } + } + + return ListView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(vertical: 0), + children: children, + ); + } + + Widget _buildSectionHeader(String letter) { + return Container( + height: headerHeight, + padding: const EdgeInsets.symmetric(horizontal: 15), + color: Colors.grey.shade100, + alignment: Alignment.centerLeft, + child: Text( + letter, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ); + } + + Widget _buildItem(Map firm) { + final name = _firmName(firm); + return InkWell( + onTap: () { + pushPage(OnboardingFullPage(scanData: firm), context); + }, + child: Container( + height: itemHeight, + padding: const EdgeInsets.symmetric(horizontal: 15), + color: Colors.white, + child: Row( + children: [ + Expanded( + child: Text( + name, + style: const TextStyle(fontSize: 15), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildAlphabetIndex(double height) { + final letters = _azIndex; + return SizedBox( + width: 20, + height: height, + child: Container( + // 背景圆角盒子占满高度 + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 4), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + // 等间隔排列字母,使它们在固定高度内均匀分布,不会被键盘压缩 + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: + letters.map((l) { + final enabled = _sections.containsKey(l); + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _jumpToLetter(l), + child: Container( + // 尽量让每个字母区域可点击,并根据启用状态改变样式 + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 4, + ), + alignment: Alignment.center, + child: Text( + l, + style: TextStyle( + fontSize: 11, + color: enabled ? Colors.blue : Colors.grey.shade400, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + }).toList(), + ), + ), + ); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } +} diff --git a/lib/pages/user/full_userinfo_page.dart b/lib/pages/user/full_userinfo_page.dart new file mode 100644 index 0000000..66709cc --- /dev/null +++ b/lib/pages/user/full_userinfo_page.dart @@ -0,0 +1,711 @@ +import 'dart:convert'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/constants/app_enums.dart'; +import 'package:qhd_prevention/customWidget/ItemWidgetFactory.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/item_list_widget.dart'; +import 'package:qhd_prevention/customWidget/photo_picker_row.dart'; +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/pages/main_tab.dart'; +import 'package:qhd_prevention/pages/mine/face_ecognition_page.dart'; +import 'package:qhd_prevention/pages/my_appbar.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/tools/id_cart_util.dart'; +import 'package:qhd_prevention/tools/tools.dart'; + +class FullUserinfoPage extends StatefulWidget { + const FullUserinfoPage({super.key, required this.isEidt, required this.isChooseFirm}); + + final bool isEidt; + final bool isChooseFirm; // 是否选择过企业 + + @override + State createState() => _FullUserinfoPageState(); +} + +class _FullUserinfoPageState extends State { + Map pd = { + "phone": "", + "name": "", + "userIdCard": "", + "birthday": "", + "gender": "", + "nationName": "", + "locationAddress": "", + "currentAddress": "", + "culturalLevel": "", + "politicalAffiliation": "", + "maritalStatus": "", + }; + Map rule = { + "name": "请输入姓名", + "userIdCard": "请输入身份证号码", + "nationName": "请选择民族", + "maritalStatus": "请选择婚姻状况", + "culturalLevel": "请选择文化程度", + "politicalAffiliation": "请选择政治面貌", + "currentAddress": "请输入现居住地址", + "locationAddress": "请输入户口所在地", + }; + late bool _isEdit; + + /// 标记修改 + late bool _isChange = false; + + String _genderText = ''; + String _birthText = ''; + String _idValue = ''; + List _idCardImgList = []; + List _idCartImgIds = []; + /// 是否修改了身份证 + bool _isChangeIdCard = false; + + List _idCardImgRemoveList = []; + + List _wenhuachengduList = []; + List _zhengzhimianmaoList = []; + List _hunyinList = [ + {"name": "已婚", "value": 1}, + {"name": "未婚", "value": 0}, + ]; + List idPhotos = []; + + @override + void initState() { + super.initState(); + _isEdit = widget.isEidt; + if (!_isEdit) { + _getUserDetail(); + } else { + pd['username'] = SessionService.instance.userName; + pd['id'] = SessionService.instance.accountId; + pd['flowFlag'] = 0; + } + _getKeyValues(); + } + + Future _getUserDetail() async { + final res = await BasicInfoApi.getUserMessage( + '${SessionService.instance.accountId}', + ); + if (res['success']) { + final data = res['data']; + _genderText = data['sex'] ?? ''; + _birthText = data['birthday'] ?? ''; + final eqForeignKey = data['userId']; + await FileApi.getImagePathWithType( + eqForeignKey, + '', + UploadFileType.idCardPhoto, + ).then((result) { + if (result['success']) { + List files = result['data'] ?? []; + _idCardImgList = + files.map((item) => item['filePath'].toString()).toList(); + _idCartImgIds = files.map((item) => item['id'].toString()).toList(); + // final filePath = fileData.first['filePath'] ?? ''; + } + }); + + setState(() { + pd = data; + try{ + final idCardBase64 = utf8.decode(base64.decode(pd['userIdCard'])); + if (idCardBase64.isNotEmpty) { + pd['userIdCard'] =idCardBase64; + } + }catch(e){ + print(e); + } + + }); + } + } + + Future _getKeyValues() async { + await BasicInfoApi.getDictValues('wenhuachengdu').then((res) { + _wenhuachengduList = res['data']; + }); + await BasicInfoApi.getDictValues('zhengzhimianmao').then((res) { + _zhengzhimianmaoList = res['data']; + }); + } + + Future _saveSuccess() async { + if (!FormUtils.hasValue(pd, 'faceFiles') && + !FormUtils.hasValue(pd, 'userAvatarUrl')) { + ToastUtil.showNormal(context, '请上传人脸图片'); + return; + } + for (String key in rule.keys) { + if (!FormUtils.hasValue(pd, key)) { + ToastUtil.showNormal(context, rule[key]); + return; + } + } + if (!FormUtils.hasValue(pd, 'userIdCard') || + !FormUtils.hasValue(pd, 'birthday')) { + ToastUtil.showNormal(context, '请输入正确身份证号'); + return ; + } + + if (idPhotos.length < 2 && !_isChange) { + ToastUtil.showNormal(context, '请上传2张身份证照片'); + return; + } + LoadingDialogHelper.show(); + // 签字上传 + final signResult = await _checkFaceImage(); + // 证件上传图片 + final situationResult = await _checkIDCartImages(); + // 删除服务器图片 + final deleteResult = await _checkDeleteImage(); + + if (signResult && situationResult && deleteResult) { + pd['userIdCard'] = base64.encode(utf8.encode(pd['userIdCard'])); + await BasicInfoApi.updateUserInfo(pd).then((res) { + LoadingDialogHelper.hide(); + if (res['success']) { + ToastUtil.showNormal(context, '保存成功'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const MainPage(isChooseFirm: false)), + ); + } else { + ToastUtil.showNormal(context, '保存失败'); + } + }); + }else{ + ToastUtil.showNormal(context, '保存失败'); + LoadingDialogHelper.hide(); + } + } + Future _checkDeleteImage() async { + late bool isSuccess = true; + if (_idCardImgRemoveList.isNotEmpty) { + final delIds = _idCardImgRemoveList; + await FileApi.deleteImages(delIds).then((result) { + if (result['success']) { + isSuccess = true; + } else { + isSuccess = false; + } + }); + }else{ + isSuccess = true; + } + return isSuccess; + + } + + /// 校验人脸照片上传 + Future _checkFaceImage() async { + final faceImgPath = pd['faceFiles'] ?? ''; + UploadFileType fileType = UploadFileType.userAvatar; + late bool isSuccess = true; + if (faceImgPath.isNotEmpty) { + try { + await FileApi.uploadFile(faceImgPath, fileType, '').then((result) { + if (result['success']) { + pd['userAvatarUrl'] = result['data']['filePath'] ?? ''; + isSuccess = true; + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '人脸照片上传失败'); + isSuccess = false; + } + }); + } catch (e) { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '签名上传失败'); + isSuccess = false; + } + } + return isSuccess; + } + + /// 校验是否需要上传身份证图片 + Future _checkIDCartImages() async { + late bool isSuccess = true; + // 身份证上传图片 + if (idPhotos.isEmpty) { + return isSuccess; + } + try { + await FileApi.uploadFiles( + idPhotos, + UploadFileType.idCardPhoto, + pd['userId'] ?? '', + ).then((result) { + if (result['success']) { + pd['userId'] = result['data']['foreignKey'] ?? ''; + } else { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '图片上传失败'); + isSuccess = false; + } + }); + } catch (e) { + ToastUtil.showNormal(context, '图片上传失败'); + isSuccess = false; + } + return isSuccess; + } + + /// 当身份证输入发生变化时调用(value 为输入框实时值) + void _onIdChanged(String value) { + _idValue = value ?? ''; + _isChangeIdCard = true; + final raw = _idValue.trim().toUpperCase(); + + // 当长度为 15 或 18 时触发解析 + if (raw.length == 15 || raw.length == 18) { + try { + final info = parseChineseIDCard(raw); + if (info.isValid /**&& info.checksumValid*/) { + setState(() { + // 使用 info.id18(标准 18 位)存储身份证号 + pd['userIdCard'] = info.id18 ?? raw; + pd['birthday'] = info.birth; // 格式 YYYY-MM-DD + pd['age'] = info.age; + pd['gender'] = info.gender; // "男"/"女" + pd['provinceCode'] = info.provinceCode; + pd['province'] = info.province; + _genderText = info.gender ?? '未知'; + _birthText = info.birth ?? '未知'; + }); + } else { + // 解析失败或校验位不正确 — 清除解析字段但保留输入 + ToastUtil.showNormal(context, '请输入正确格式身份证号'); + + setState(() { + pd.remove('birthday'); + pd.remove('age'); + pd.remove('gender'); + pd.remove('provinceCode'); + pd.remove('province'); + _genderText = '输入身份证获取'; + _birthText = '输入身份证获取'; + }); + } + } catch (e) { + ToastUtil.showNormal(context, '请输入正确格式身份证号'); + // 出现异常则清除解析字段 + setState(() { + pd.remove('birthday'); + pd.remove('age'); + pd.remove('gender'); + pd.remove('provinceCode'); + pd.remove('province'); + _genderText = '输入身份证获取'; + _birthText = '输入身份证获取'; + }); + } + } else { + // 长度不足时清除解析结果(可按需注释掉保留旧解析) + if (_genderText != '请选择' || _birthText != '请选择') { + setState(() { + pd.remove('birthday'); + pd.remove('age'); + pd.remove('gender'); + pd.remove('provinceCode'); + pd.remove('province'); + _genderText = '输入身份证获取'; + _birthText = '输入身份证获取'; + }); + } + } + } + + @override + Widget build(BuildContext context) { + bool isShow = _isEdit; + if (!_isEdit && FormUtils.hasValue(pd, 'id')) { + isShow = true; + } + return Scaffold( + appBar: MyAppbar( + title: '信息补充', + isBack: (!_isEdit || _isChange), + onBackPressed: () async { + if (_isChange) { + await CustomAlertDialog.showConfirm( + context, + title: '温馨提示', + content: '是否放弃修改?', + cancelText: '取消', + ).then( + (isSure) => { + if (isSure) {Navigator.pop(context)}, + }, + ); + } else { + Navigator.pop(context); + } + }, + actions: [ + if (!_isEdit) + TextButton( + onPressed: () { + setState(() { + _isChange = true; + _isEdit = true; + }); + }, + child: Text( + '修改', + style: TextStyle(color: Colors.white, fontSize: 17), + ), + ), + ], + ), + + body: SafeArea( + child: ItemListWidget.itemContainer( + horizontal: 5, + isShow + ? ListView( + children: [ + RepairedPhotoSection( + title: '人脸照片', + inlineSingle: true, + isRequired: _isEdit, + initialMediaPaths: + FormUtils.hasValue(pd, 'userAvatarUrl') + ? [ + '${ApiService.baseImgPath}${pd['userAvatarUrl'] ?? ''}', + ] + : [], + horizontalPadding: _isEdit ? 12 : 0, + inlineImageWidth: 60, + isFaceImage: true, + isEdit: _isEdit, + onChanged: (files) { + if (files.isEmpty) { + return; + } + pd['faceFiles'] = files.first.path; + }, + onAiIdentify: () {}, + // onMediaRemovedForIndex: (index) async { + // final deleFile = pd['userAvatarUrl'] ?? ''; + // if (deleFile.contains(UploadFileType.idCardPhoto.path)) { + // _idCardImgRemoveList.add(deleFile); + // } + // }, + ), + if (_isEdit) + ItemListWidget.itemContainer( + const Text( + '温馨提示:该照片为进入项目施工场所口门人脸识别使用', + style: TextStyle(color: Colors.red, fontSize: 10), + ), + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '姓名:', + isRequired: true, + hintText: '请输入姓名', + text: pd['name'] ?? '', + isEditable: _isEdit, + onChanged: (value) { + pd['name'] = value; + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '手机号:', + isRequired: true, + text: pd['username'] ?? '', + isNumericInput: true, + hintText: '请输入手机号', + strongRequired: _isEdit, + isEditable: false, + ), + const Divider(), + // 身份证输入:只使用 onChanged 回调(value 是实时输入框的值) + ItemListWidget.singleLineTitleText( + label: '身份证:', + isRequired: true, + hintText: '请输入身份证号', + text: pd['userIdCard'] ?? '', + isEditable: _isEdit, + onChanged: (value) { + // value 是实时输入框值 + _onIdChanged(value ?? ''); + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '民族:', + isEditable: _isEdit, + text: pd['nationName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + final found = await BottomPicker.show( + context, + items: nationMapList, + itemBuilder: + (i) => + Text(i['name']!, textAlign: TextAlign.center), + initialIndex: 0, + ); + //FocusHelper.clearFocus(context); + + if (found != null) { + setState(() { + pd['nationName'] = found['name']; + pd['nation'] = found['code']; + }); + } + }, + ), + const Divider(), + // 性别:不可编辑,显示解析结果(也允许手动覆盖) + ItemListWidget.selectableLineTitleTextRightButton( + label: '性别:', + isEditable: false, + text: _genderText, + + strongRequired: _isEdit, + isRequired: true, + onTap: () { + // 允许手动选择覆盖解析结果 + showModalBottomSheet( + context: context, + builder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: const Text('男'), + onTap: () { + setState(() { + pd['gender'] = '男'; + _genderText = '男'; + }); + Navigator.pop(context); + }, + ), + ListTile( + title: const Text('女'), + onTap: () { + setState(() { + pd['gender'] = '女'; + _genderText = '女'; + }); + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + }, + ), + const Divider(), + // 出生年月:显示解析结果,但仍然可以手动修改 + ItemListWidget.selectableLineTitleTextRightButton( + label: '出生年月:', + isEditable: false, + text: _birthText, + strongRequired: _isEdit, + isRequired: true, + onTap: () async { + }, + ), + const Divider(), + + ItemListWidget.singleLineTitleText( + label: '户口所在地:', + isRequired: _isEdit, + hintText: '请输入户口所在地', + text: pd['locationAddress'] ?? '', + isEditable: _isEdit, + onChanged: (value) { + pd['locationAddress'] = value; + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '现住址:', + isRequired: true, + text: pd['currentAddress'] ?? '', + hintText: '请输入现住址', + isEditable: _isEdit, + onChanged: (value) { + pd['currentAddress'] = value; + }, + ), + const Divider(), + if (_isEdit || _idCardImgList.isNotEmpty) + RepairedPhotoSection( + title: '身份证照片', + isRequired: _isEdit, + maxCount: 2, + initialMediaPaths: + _idCardImgList + .map( + (item) => + ApiService.baseImgPath + item, + ) + .toList(), + isEdit: _isEdit, + horizontalPadding: _isEdit ? 12 : 0, + inlineImageWidth: 60, + onChanged: (files) { + idPhotos = files.map((file) => file.path).toList(); + }, + onMediaRemovedForIndex: (index) async { + final deleFile = _idCardImgList[index]; + final deleId = _idCartImgIds[index]; + if (deleFile.contains(UploadFileType.idCardPhoto.path)) { + _idCardImgList.removeAt(index); + _idCartImgIds.removeAt(index); + _idCardImgRemoveList.add(deleId); + } + }, + + onAiIdentify: () { + /* ... */ + }, + ), + if (_isEdit) + ItemListWidget.itemContainer( + const Text( + '温馨提示:用户要上传身份证正反面(身份证照片数量是2张才能进行人员培训)', + style: TextStyle(color: Colors.red, fontSize: 10), + ), + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '文化程度:', + isEditable: _isEdit, + text: pd['culturalLevelName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + final found = await BottomPicker.show( + context, + items: _wenhuachengduList, + itemBuilder: + (i) => Text( + i['dictLabel']!, + textAlign: TextAlign.center, + ), + initialIndex: 0, + ); + //FocusHelper.clearFocus(context); + + if (found != null) { + setState(() { + pd['culturalLevelName'] = found['dictLabel']; + pd['culturalLevel'] = found['dictValue']; + }); + } + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '政治面貌:', + isEditable: _isEdit, + text: pd['politicalAffiliationName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + final found = await BottomPicker.show( + context, + items: _zhengzhimianmaoList, + itemBuilder: + (i) => Text( + i['dictLabel']!, + textAlign: TextAlign.center, + ), + initialIndex: 0, + ); + //FocusHelper.clearFocus(context); + + if (found != null) { + setState(() { + pd['politicalAffiliationName'] = found['dictLabel']; + pd['politicalAffiliation'] = found['dictValue']; + }); + } + }, + ), + const Divider(), + ItemListWidget.selectableLineTitleTextRightButton( + label: '婚姻状态:', + isEditable: _isEdit, + text: pd['maritalStatusName'] ?? '请选择', + isRequired: _isEdit, + onTap: () async { + final found = await BottomPicker.show( + context, + items: _hunyinList, + itemBuilder: + (i) => + Text(i['name']!, textAlign: TextAlign.center), + initialIndex: 0, + ); + //FocusHelper.clearFocus(context); + + if (found != null) { + setState(() { + pd['maritalStatusName'] = found['name']; + pd['maritalStatus'] = found['value']; + }); + } + }, + ), + const Divider(), + ListItemFactory.createYesNoSection( + title: "是否流动人员:", + horizontalPadding: 2, + verticalPadding: 0, + yesLabel: "是", + noLabel: "否", + text: pd['flowFlag'] == 1 ? '是' : '否', + isRequired: true, + isEdit: _isEdit, + groupValue: (pd['flowFlag'] ?? 0) == 1, + onChanged: (val) { + setState(() { + pd['flowFlag'] = val ? 1 : 0; + }); + }, + ), + const Divider(), + ItemListWidget.singleLineTitleText( + label: '电子邮箱:', + isRequired: false, + isNumericInput: false, + hintText: '请输入电子邮箱', + text: pd['email'] ?? '', + isEditable: _isEdit, + onChanged: (value) { + pd['email'] = value; + }, + ), + const Divider(), + const SizedBox(height: 20), + if (_isEdit) + CustomButton( + text: '保存', + backgroundColor: Colors.blue, + onPressed: () { + _saveSuccess(); + }, + ), + ], + ) + : const SizedBox(), + ), + ), + ); + } +} diff --git a/lib/pages/user/homework_entrance.dart b/lib/pages/user/homework_entrance.dart new file mode 100644 index 0000000..4baf2e0 --- /dev/null +++ b/lib/pages/user/homework_entrance.dart @@ -0,0 +1,157 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/customWidget/item_list_widget.dart'; + + +class HomeworkEntrance extends StatefulWidget { + const HomeworkEntrance(this.title, {super.key}); + + final String title; + + @override + State createState() => HomeworkEntranceState(); +} + +class HomeworkEntranceState extends State { + + // 设置项状态 + bool notificationsEnabled = false; + bool passwordChanged = false; + bool updateAvailable = false; + bool logoutSelected = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xFFF3F4F7), + body: SingleChildScrollView( + // child: Padding( + // padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部标题区域 + _buildHeaderSection(), + const SizedBox(height: 20), + // 底部按钮 + // _buildActionButton(), + ], + ), + // ), + ), + ); + } + + Widget _buildHeaderSection() { + return Stack( + alignment: const FractionalOffset(0.5, 0), + children: [ + + + Padding( + padding: EdgeInsets.fromLTRB(0,0,0,10), + child: Image.asset( + "assets/images/xiangguan_rukou_bg.png", + width: MediaQuery.of(context).size.width, // 获取屏幕宽度 + fit: BoxFit.cover, + ) + ), + + Positioned( + top: 51, + child: Text(widget.title,style: TextStyle(color: Colors.white,fontSize: 20,fontWeight: FontWeight.bold,),), + ), + + + //返回按钮 + Positioned( + top: 2, + left: 0, + child: Padding( + padding: EdgeInsets.only(left: Platform.isIOS ? 4.0 : 8.0,top: 40), + child: IconButton( + icon: Icon( + Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back, + color: Colors.white, + size: Platform.isIOS ? 20.0 : 24.0, // iOS使用更小图标 + ), + padding: EdgeInsets.all(0), // 移除默认内边距 + constraints: const BoxConstraints(), // 移除默认约束 + onPressed: () => Navigator.of(context).pop(), + ), + ), + ), + + + // 我的工作区 + _buildSettingsList(), + // _buildWorkSection(), + + ], + + + ); + } + + + Widget _buildSettingsList() { + return + Container( + margin: EdgeInsets.fromLTRB(0,190,0,0), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + // margin: const EdgeInsets.fromLTRB(20,0,20,0), + decoration: BoxDecoration( + color: Color(0xFFF3F4F7), + borderRadius:BorderRadius.only(topLeft:Radius.circular(30) ,topRight: Radius.circular(30)) , + + + ), + child: Column( + children: [ + + SizedBox(height: 30,), + + GestureDetector( + child: _setItemWidget("申请","assets/images/xiangguan_rukou1.png"), + onTap: () { + + }, + ), + SizedBox(height: 5,), + + GestureDetector( + child: _setItemWidget("待办","assets/images/xiangguan_rukou2.png"), + onTap: () { + + }, + ), + SizedBox(height: 5,), + + GestureDetector( + child: _setItemWidget("已办","assets/images/xiangguan_rukou3.png"), + onTap: () { + + }, + ), + SizedBox(height: 5,), + + ], + ), + ); + } + + + + Widget _setItemWidget(final String text,final String imagePath) { + return ItemListWidget.OneRowImageArrowTitle( + label: text, + imgPath: imagePath, + ); + } + + +} + + + diff --git a/lib/pages/user/login_page.dart b/lib/pages/user/login_page.dart new file mode 100644 index 0000000..e40792e --- /dev/null +++ b/lib/pages/user/login_page.dart @@ -0,0 +1,622 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.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/mine/forgot_pwd_page.dart'; +import 'package:qhd_prevention/pages/mine/mine_set_pwd_page.dart'; +import 'package:qhd_prevention/pages/mine/webViewPage.dart'; +import 'package:qhd_prevention/pages/user/CustomInput.dart'; +import 'package:qhd_prevention/pages/user/choose_userFirm_page.dart'; +import 'package:qhd_prevention/pages/user/full_userinfo_page.dart'; +import 'package:qhd_prevention/pages/user/register_page.dart'; +import 'package:qhd_prevention/services/auth_service.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:qhd_prevention/pages/main_tab.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _codeController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + final FocusNode _phoneFocusNode = FocusNode(); + final FocusNode _passwordFocusNode = FocusNode(); + final FocusNode _codeFocusNode = FocusNode(); + + String _errorMessage = ''; + bool _isLoading = false; + bool _obscurePassword = true; + bool _agreed = false; + String _captchaImageBase64 = ''; + String _captchaIdentifier = ''; + bool _rememberPassword = true; + + @override + void initState() { + super.initState(); + _phoneController.addListener(_onTextChanged); + _getData(); + _getCaptcha(); + _phoneController.text = SessionService.instance.loginPhone ?? ""; + _passwordController.text = SessionService.instance.loginPass ?? ""; + } + + @override + void dispose() { + _phoneController.removeListener(_onTextChanged); + _phoneController.dispose(); + _passwordController.dispose(); + _codeController.dispose(); + _phoneFocusNode.dispose(); + _passwordFocusNode.dispose(); + _codeFocusNode.dispose(); + super.dispose(); + } + + Future _getCaptcha() async { + final response = await AuthApi.getUserCaptcha(); + if (response['success']) { + setState(() { + _captchaImageBase64 = response['data']['img']; + _captchaIdentifier = response['data']['captchaKey']; + }); + } + } + + Future _getData() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _phoneController.text = prefs.getString('savePhone') ?? ''; + _passwordController.text = prefs.getString('savePass') ?? ''; + }); + } + + Future _saveData(String phone, String pass) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString("savePhone", phone); + await prefs.setString("savePass", pass); + } + + void _onTextChanged() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + double height = 230.0; + return Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: true, + body: Stack( + children: [ + // 背景图:铺满屏幕 + Positioned( + left: 0, + top: 0, + right: 0, + child: Image.asset( + 'assets/images/loginbg.png', + fit: BoxFit.fitWidth, + ), + ), + Positioned( + top: 0, + left: 20, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 70), // 顶部间距 + Image.asset( + "assets/images/logo.png", + width: 40, + height: 40, + ), + const SizedBox(height: 10), + const Text( + "欢迎使用", + style: TextStyle( + color: Colors.white, + fontSize: 23, + fontWeight: FontWeight.w500, + ), + ), + const Text( + "秦港-相关方安全管理平台", + style: TextStyle( + color: Colors.white, + fontSize: 23, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + top: height, + child: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: SafeArea( + child: Form( + key: _formKey, + child: SingleChildScrollView( + // 让内容至少占满屏高,并且内容可以滚动 + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: screenHeight-height-50), + child: IntrinsicHeight( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 中间可滚动表单区域(左右内边距) + Container( + // color: Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // const SizedBox(height: 60), + CustomInput.buildInput( + _phoneController, + title: "手机号", + hint: "请输入您的手机号", + keyboardType: TextInputType.phone, + suffix: + _phoneController.text.isEmpty + ? SizedBox() + : IconButton( + icon: const Icon( + Icons.cancel, + size: 20, + color: Colors.grey, + ), + onPressed: + () => setState( + () => + _phoneController + .clear(), + ), + ), + validator: (v) { + if (v == null || v.isEmpty) + return '请输入您的手机号'; + return null; + }, + ), + const SizedBox(height: 20), + + CustomInput.buildInput( + title: "密码", + _passwordController, + hint: "请输入您的密码", + obscure: _obscurePassword, + suffix: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility_off + : Icons.visibility, + color: Colors.grey, + ), + onPressed: + () => setState( + () => + _obscurePassword = + !_obscurePassword, + ), + ), + validator: (v) { + if (v == null || v.isEmpty) + return '请输入密码'; + return null; + }, + ), + + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + '验证码', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + + // 验证码行 + SizedBox( + height: 60, + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 0, + right: 10, + ), + child: TextFormField( + controller: _codeController, + focusNode: _codeFocusNode, + keyboardType: + TextInputType.number, + decoration: const InputDecoration( + hintText: '请输入验证码', + hintStyle: TextStyle( + color: Colors.black26, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + style: const TextStyle( + color: Colors.black, + ), + ), + ), + ), + + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + ), + child: _buildCaptchaImage(), + ), + ], + ), + ), + const Divider( + height: 0.5, + color: Colors.black26, + ), + + const SizedBox(height: 10), + + if (_errorMessage.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 25, + ), + child: Text( + _errorMessage, + style: const TextStyle( + color: Colors.red, + ), + ), + ), + + _remenbemberPWDAndRegister(), + const SizedBox(height: 10), + + CustomButton( + text: '登录', + backgroundColor: const Color(0xFF2A75F8), + height: 50, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + borderRadius: 25, + onPressed: _handleLogin, + ), + const SizedBox(height: 10), + CustomButton( + text: '注册', + height: 50, + textColor: Colors.black87, + textStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + backgroundColor: Color(0xFFF3F4F8), + borderRadius: 25, + onPressed: () { + pushPage(RegisterPage(), context); + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + + // 底部协议:固定在页面底部(不会被背景覆盖,因为在上层) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + value: _agreed, + activeColor: Colors.blue, + checkColor: Colors.white, + side: const BorderSide(color: Colors.grey), + onChanged: (value) { + setState(() { + _agreed = value ?? false; + }); + }, + ), + Flexible( + child: RichText( + text: TextSpan( + children: [ + const TextSpan( + text: '我已阅读并同意', + style: TextStyle( + color: Colors.black, + fontSize: 12, + ), + ), + TextSpan( + text: '《服务协议》', + style: const TextStyle( + color: Colors.blue, + fontSize: 12, + ), + // 如果你用 recognizer,请替换为你之前的 recognizer 变量 + recognizer: + TapGestureRecognizer() + ..onTap = () { + pushPage( + const WebViewPage( + name: "用户服务协议", + url: + 'http://47.92.102.56:7811/file/xieyi/zsyhxy.htm', + ), + context, + ); + }, + ), + const TextSpan( + text: '和', + style: TextStyle( + color: Colors.black, + fontSize: 12, + ), + ), + TextSpan( + text: '《隐私政策》', + style: const TextStyle( + color: Colors.blue, + fontSize: 12, + ), + recognizer: + TapGestureRecognizer() + ..onTap = () { + pushPage( + const WebViewPage( + name: "隐私政策", + url: + 'http://47.92.102.56:7811/file/xieyi/zsysq.htm', + ), + context, + ); + }, + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ), + // 交互层:放在背景之上 + + ], + ), + ); + } + + Widget _remenbemberPWDAndRegister() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Row( + children: [ + // 左侧:记住密码 + Expanded( + child: InkWell( + onTap: + () => setState(() => _rememberPassword = !_rememberPassword), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: _rememberPassword, + onChanged: + (v) => setState(() => _rememberPassword = v ?? false), + activeColor: const Color(0xFF2A75F8), + side: const BorderSide(color: Colors.grey), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + const Text( + '记住密码', + style: TextStyle(fontSize: 14, color: Colors.black38), + ), + ], + ), + ), + ), + + TextButton( + onPressed: () { + pushPage(ForgotPwdPage('0'), context); + }, + child: const Text( + '忘记密码?', + style: TextStyle(fontSize: 14, color: Colors.black38), + ), + ), + ], + ), + ); + } + + // 修改验证码图片构建方法 + 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: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.memory( + base64.decode(_captchaImageBase64), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Text( + '加载失败', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ); + }, + ), + ), + ), + ); + } + + Future _handleLogin() async { + if (_isLoading) { + return; + } + if (!(_formKey.currentState?.validate() ?? false)) return; + if (_codeController.text.isEmpty) { + ToastUtil.showNormal(context, "请输入验证码"); + return; + } + if (!_agreed) { + ToastUtil.showNormal(context, "请先阅读并同意《服务协议》和《隐私政策》"); + return; + } + + // _phoneController.text='18700000002'; + // _passwordController.text='Aa@12345678'; + // _phoneController.text='卓云企业1'; + // _phoneController.text='ceshi36-220'; + // _passwordController.text='Aa12345678'; + final userName = _phoneController.text.trim(); + final userPwd = _passwordController.text.trim(); + + _saveData(userName, userPwd); + + setState(() => _isLoading = true); + + Map params = { + 'captchaCode': _codeController.text, + 'captchaKey' : _captchaIdentifier, + }; + LoadingDialogHelper.show(); + try { + final data = await AuthService.login(params, userName, userPwd); + LoadingDialogHelper.hide(); + _getCaptcha(); + setState(() => _isLoading = false); + if (FormUtils.hasValue(data, 'success') && + data['success']) { // 登录成功直接进入主页 + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const MainPage(isChooseFirm: true,)), + ); + } else { + if (FormUtils.hasValue(data, 'isInfoComplete') && + data['isInfoComplete'] == false) { // 如果还没有用户信息。需要先完善 + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const FullUserinfoPage(isEidt: true, isChooseFirm: false,)), + ); + } else if (FormUtils.hasValue(data, 'isChooseFirm') && + data['isChooseFirm'] == false) { // 多个企业,跳转选择 + { // 先不进行底座登录,选择企业 + List firmList = data['firmList']; + if (firmList.isEmpty) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const MainPage(isChooseFirm: false,)), + ); + return; + }else{ // 跳转选择企业 + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ChooseUserfirmPage(firms: firmList, userName: data['userName'], password: data['password'],)), + ); + } + + } + } + if (data.isEmpty) { + return; + } + } + }catch (e) { + setState(() => _isLoading = false); + Fluttertoast.showToast(msg: '登录失败: $e'); + } + } +} diff --git a/lib/pages/user/register_page.dart b/lib/pages/user/register_page.dart new file mode 100644 index 0000000..606a855 --- /dev/null +++ b/lib/pages/user/register_page.dart @@ -0,0 +1,325 @@ +import 'dart:async'; +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/pages/my_appbar.dart'; +import 'package:qhd_prevention/pages/user/CustomInput.dart'; +import 'package:qhd_prevention/pages/user/login_page.dart'; +import 'package:qhd_prevention/tools/tools.dart'; +import '../../http/ApiService.dart'; // 假设你的 API 在这里 + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + + @override + State createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _formKey = GlobalKey(); + + final TextEditingController _phoneController = TextEditingController(); + final TextEditingController _codeController = TextEditingController(); + final TextEditingController _pwdController = TextEditingController(); + final TextEditingController _confirmPwdController = TextEditingController(); + + bool _obscurePwd = true; + bool _obscureConfirm = true; + + // 验证码发送状态和倒计时 + bool _isSendingCode = false; + int _secondsLeft = 0; + Timer? _timer; + + String textString = + "*密码长度8-18位,必须包含大小写字母+数字+特殊字母,例如:Qa@123456"; + + @override + void dispose() { + _timer?.cancel(); + _phoneController.dispose(); + _codeController.dispose(); + _pwdController.dispose(); + _confirmPwdController.dispose(); + super.dispose(); + } + + // 验证密码复杂度 + bool isPasswordValid(String password) { + final hasUpperCase = RegExp(r'[A-Z]'); + final hasLowerCase = RegExp(r'[a-z]'); + final hasNumber = RegExp(r'[0-9]'); + final hasSpecialChar = RegExp(r'[!@#\$%\^&\*\(\)_\+\-=\[\]\{\};:"\\|,.<>\/\?~`]'); + return hasUpperCase.hasMatch(password) && + hasLowerCase.hasMatch(password) && + hasNumber.hasMatch(password) && + hasSpecialChar.hasMatch(password); + } + + // 手机号简单校验(11 位数字) + bool _isPhoneValid(String phone) { + final RegExp phoneReg = RegExp(r'^\d{11}$'); + return phoneReg.hasMatch(phone); + } + + void _startCountdown(int seconds) { + _timer?.cancel(); + setState(() { + _secondsLeft = seconds; + }); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (!mounted) return; + setState(() { + _secondsLeft--; + if (_secondsLeft <= 0) { + _timer?.cancel(); + _secondsLeft = 0; + } + }); + }); + } + + Future _sendCode() async { + final phone = _phoneController.text.trim(); + if (phone.isEmpty) { + ToastUtil.showNormal(context, '请输入手机号'); + return; + } + if (!_isPhoneValid(phone)) { + ToastUtil.showNormal(context, '请输入有效的手机号(11位)'); + return; + } + + if (_isSendingCode || _secondsLeft > 0) return; + + setState(() { + _isSendingCode = true; + }); + + LoadingDialogHelper.show(); + try { + final resp = await BasicInfoApi.sendRegisterSms({'phone': phone}); + LoadingDialogHelper.hide(); + + if (resp != null && resp['success'] == true) { + ToastUtil.showNormal(context, '验证码已发送'); + _startCountdown(60); + } else { + ToastUtil.showNormal(context, resp?['message'] ?? '发送验证码失败'); + } + } catch (e) { + LoadingDialogHelper.hide(); + ToastUtil.showNormal(context, '发送验证码失败,请稍后重试'); + } finally { + setState(() { + _isSendingCode = false; + }); + } + } + + Future _handleRegister() async { + if (!(_formKey.currentState?.validate() ?? false)) return; + + final phone = _phoneController.text.trim(); + final code = _codeController.text.trim(); + final pwd = _pwdController.text.trim(); + final confirm = _confirmPwdController.text.trim(); + + if (!_isPhoneValid(phone)) { + ToastUtil.showNormal(context, '请输入有效的手机号(11位)'); + return; + } + if (code.isEmpty) { + ToastUtil.showNormal(context, '请输入验证码'); + return; + } + if (pwd.isEmpty) { + ToastUtil.showNormal(context, '请输入密码'); + return; + } + if (confirm.isEmpty) { + ToastUtil.showNormal(context, '请确认密码'); + return; + } + if (pwd != confirm) { + ToastUtil.showNormal(context, '两次输入的密码不一致'); + return; + } + if (pwd.length < 8) { + ToastUtil.showNormal(context, '密码长度需至少8位'); + return; + } + if (pwd.length > 32) { + ToastUtil.showNormal(context, '密码长度需小于32位'); + return; + } + if (!isPasswordValid(pwd)) { + ToastUtil.showNormal(context, '密码必须包含大小写字母、数字和特殊符号'); + return; + } + + try { + final data = { + 'phone': phone, + 'phoneCode': code, + 'newPassword': pwd, + 'confirmPassword': pwd, + }; + final resp = await BasicInfoApi.register(data); + + if (resp != null && resp['success'] == true) { + ToastUtil.showNormal(context, '注册成功,请登录'); + // 跳转到登录页并移除当前页面栈 + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), + (route) => false, + ); + } else { + ToastUtil.showNormal(context, resp?['message'] ?? '注册失败,请重试'); + } + } catch (e) { + ToastUtil.showNormal(context, '注册失败,请稍后重试'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: MyAppbar(title: '注册账号'), + backgroundColor: Colors.white, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 8), + + // 手机号(使用 CustomInput) + CustomInput.buildInput( + _phoneController, + title: '手机号', + hint: '请输入手机号', + keyboardType: TextInputType.phone, + validator: (v) { + if (v == null || v.isEmpty) return '请输入手机号'; + if (!_isPhoneValid(v.trim())) return '请输入有效的手机号'; + return null; + }, + ), + + const SizedBox(height: 16), + + // 验证码 + 发送按钮 行(验证码输入使用 CustomInput) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: CustomInput.buildInput( + _codeController, + title: '验证码', + hint: '请输入验证码', + keyboardType: TextInputType.number, + validator: (v) { + if (v == null || v.isEmpty) return '请输入验证码'; + return null; + }, + ), + ), + const SizedBox(width: 12), + Column( + + children: [ + const SizedBox(height: 40), + SizedBox( + height: 40, + child: ElevatedButton( + onPressed: (_secondsLeft > 0 || _isSendingCode) ? null : _sendCode, + style: ElevatedButton.styleFrom( + backgroundColor: (_secondsLeft > 0) ? Colors.grey.shade400 : const Color(0xFF2A75F8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.symmetric(horizontal: 12), + ), + child: Text( + _secondsLeft > 0 ? '$_secondsLeft s后可重发' : '发送验证码', + style: const TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ], + ) + ], + ), + + const SizedBox(height: 16), + + // 密码 + CustomInput.buildInput( + _pwdController, + title: '密码', + hint: '请输入密码', + obscure: _obscurePwd, + suffix: IconButton( + icon: Icon(_obscurePwd ? Icons.visibility_off : Icons.visibility, color: Colors.grey), + onPressed: () => setState(() => _obscurePwd = !_obscurePwd), + ), + validator: (v) { + if (v == null || v.isEmpty) return '请输入密码'; + if (v.length < 8) return '密码长度至少 8 位'; + return null; + }, + ), + + const SizedBox(height: 16), + + // 确认密码 + CustomInput.buildInput( + _confirmPwdController, + title: '确认密码', + hint: '请再次输入密码', + obscure: _obscureConfirm, + suffix: IconButton( + icon: Icon(_obscureConfirm ? Icons.visibility_off : Icons.visibility, color: Colors.grey), + onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), + ), + validator: (v) { + if (v == null || v.isEmpty) return '请确认密码'; + return null; + }, + ), + + const SizedBox(height: 12), + + // 密码提示语 + Align( + alignment: Alignment.centerLeft, + child: Text( + textString, + style: const TextStyle(color: Colors.red, fontSize: 13), + ), + ), + + const SizedBox(height: 30), + + // 注册确认按钮 + SizedBox( + width: double.infinity, + height: 45, + child: CustomButton( + onPressed: _handleRegister, + text: '确认', + backgroundColor: Colors.blue, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/rename_script.sh b/lib/rename_script.sh new file mode 100644 index 0000000..cb0cb0f --- /dev/null +++ b/lib/rename_script.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# 文件名替换脚本:将文件名中的指定旧字符串替换为新字符串 +# 用法:./rename_script.sh /目标/目录/路径 "旧字符串" "新字符串" + +# 检查参数数量 +if [ $# -ne 3 ]; then + echo "错误:参数数量不正确" + echo "用法: $0 /目录/路径 \"旧字符串\" \"新字符串\"" + exit 1 +fi + +target_dir="$1" +old_str="$2" +new_str="$3" + +# 检查目录是否存在 +if [ ! -d "$target_dir" ]; then + echo "错误:目录 '$target_dir' 不存在" + exit 1 +fi + +# 设置安全选项 +set -euo pipefail +IFS=$'\n' # 正确处理文件名中的空格 + +echo "开始处理目录: $target_dir" +echo "替换规则: '$old_str' -> '$new_str'" +echo "----------------------------------------" + +# 查找所有包含旧字符串的文件名 +counter=0 +while IFS= read -r -d $'\0' file; do + # 获取文件名和目录路径 + dirpath=$(dirname "$file") + filename=$(basename "$file") + + # 检查是否需要替换 + if [[ "$filename" == *"$old_str"* ]]; then + # 执行替换 + new_name="${filename//$old_str/$new_str}" + new_path="$dirpath/$new_name" + + # 跳过名称未变化的文件 + if [[ "$filename" == "$new_name" ]]; then + continue + fi + + # 避免覆盖已存在文件 + if [ -e "$new_path" ]; then + echo "警告: 跳过 '$file' -> '$new_name' (目标文件已存在)" + continue + fi + + # 执行重命名 + mv -v "$file" "$new_path" + ((counter++)) + fi +done < <(find "$target_dir" -type f -print0 2>/dev/null) + +echo "----------------------------------------" +echo "完成! 已重命名 $counter 个文件" \ No newline at end of file diff --git a/lib/services/SessionService.dart b/lib/services/SessionService.dart new file mode 100644 index 0000000..2a866d9 --- /dev/null +++ b/lib/services/SessionService.dart @@ -0,0 +1,415 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class BizAttr { + final String? mobileRedirectUrl; + final String? webRedirectUrl; + + BizAttr({this.mobileRedirectUrl, this.webRedirectUrl}); + + factory BizAttr.fromJson(Map? json) { + if (json == null) return BizAttr(); + return BizAttr( + mobileRedirectUrl: json['mobileRedirectUrl'] as String?, + webRedirectUrl: json['webRedirectUrl'] as String?, + ); + } + + Map toJson() => { + 'mobileRedirectUrl': mobileRedirectUrl, + 'webRedirectUrl': webRedirectUrl, + }; +} + +class UserData { + final String? id; + final String? userId; + final String? username; + final String? name; + final String? corpinfoId; + final String? corpinfoName; + final int? mainCorpFlag; + final int? userType; + final String? departmentId; + final String? departmentName; + final String? postId; + final String? postName; + final String? roleId; + final String? email; + final String? personnelType; + final String? personnelTypeName; + final String? nation; + final String? nationName; + final String? userIdCard; + final String? userAvatarUrl; + final String? currentAddress; + final String? locationAddress; + final dynamic rankLevel; + final String? rankLevelName; + final String? phone; + final int? sort; + final int? version; + final String? createId; + final String? createName; + final String? createTime; + final String? updateId; + final String? updateName; + final String? updateTime; + final String? remarks; + final String? deleteEnum; + final String? tenantId; + final String? orgId; + final String? env; + final int? departmentLeaderFlag; + final int? deputyLeaderFlag; + final String? culturalLevel; + final String? culturalLevelName; + final String? maritalStatus; + final String? maritalStatusName; + final String? politicalAffiliation; + final String? politicalAffiliationName; + final String? mappingName; + final String? mappingUserName; + final String? mappingPostName; + final String? mappingDeptName; + final dynamic employmentFlag; + + UserData({ + this.id, + this.userId, + this.username, + this.name, + this.corpinfoId, + this.corpinfoName, + this.mainCorpFlag, + this.userType, + this.departmentId, + this.departmentName, + this.postId, + this.postName, + this.roleId, + this.email, + this.personnelType, + this.personnelTypeName, + this.nation, + this.nationName, + this.userIdCard, + this.userAvatarUrl, + this.currentAddress, + this.locationAddress, + this.rankLevel, + this.rankLevelName, + this.phone, + this.sort, + this.version, + this.createId, + this.createName, + this.createTime, + this.updateId, + this.updateName, + this.updateTime, + this.remarks, + this.deleteEnum, + this.tenantId, + this.orgId, + this.env, + this.departmentLeaderFlag, + this.deputyLeaderFlag, + this.culturalLevel, + this.culturalLevelName, + this.maritalStatus, + this.maritalStatusName, + this.politicalAffiliation, + this.politicalAffiliationName, + this.mappingName, + this.mappingUserName, + this.mappingPostName, + this.mappingDeptName, + this.employmentFlag, + }); + + factory UserData.fromJson(Map json) { + return UserData( + id: json['id']?.toString(), + userId: json['userId']?.toString(), + username: json['username'] as String?, + name: json['name'] as String?, + corpinfoId: json['corpinfoId']?.toString(), + corpinfoName: json['corpinfoName'] as String?, + mainCorpFlag: json['mainCorpFlag'] as int?, + userType: json['userType'] as int?, + departmentId: json['departmentId']?.toString(), + departmentName: json['departmentName'] as String?, + postId: json['postId']?.toString(), + postName: json['postName'] as String?, + roleId: json['roleId']?.toString(), + email: json['email'] as String?, + personnelType: json['personnelType'] as String?, + personnelTypeName: json['personnelTypeName'] as String?, + nation: json['nation'] as String?, + nationName: json['nationName'] as String?, + userIdCard: json['userIdCard'] as String?, + userAvatarUrl: json['userAvatarUrl'] as String?, + currentAddress: json['currentAddress'] as String?, + locationAddress: json['locationAddress'] as String?, + rankLevel: json['rankLevel'], + rankLevelName: json['rankLevelName'] as String?, + phone: json['phone'] as String?, + sort: json['sort'] as int?, + version: json['version'] as int?, + createId: json['createId']?.toString(), + createName: json['createName'] as String?, + createTime: json['createTime'] as String?, + updateId: json['updateId']?.toString(), + updateName: json['updateName'] as String?, + updateTime: json['updateTime'] as String?, + remarks: json['remarks'] as String?, + deleteEnum: json['deleteEnum'] as String?, + tenantId: json['tenantId']?.toString(), + orgId: json['orgId']?.toString(), + env: json['env'] as String?, + departmentLeaderFlag: json['departmentLeaderFlag'] as int?, + deputyLeaderFlag: json['deputyLeaderFlag'] as int?, + culturalLevel: json['culturalLevel'] as String?, + culturalLevelName: json['culturalLevelName'] as String?, + maritalStatus: json['maritalStatus'] as String?, + maritalStatusName: json['maritalStatusName'] as String?, + politicalAffiliation: json['politicalAffiliation'] as String?, + politicalAffiliationName: json['politicalAffiliationName'] as String?, + mappingName: json['mappingName'] as String?, + mappingUserName: json['mappingUserName'] as String?, + mappingPostName: json['mappingPostName'] as String?, + mappingDeptName: json['mappingDeptName'] as String?, + employmentFlag: json['employmentFlag'], + ); + } + + Map toJson() { + return { + 'id': id, + 'userId': userId, + 'username': username, + 'name': name, + 'corpinfoId': corpinfoId, + 'corpinfoName': corpinfoName, + 'mainCorpFlag': mainCorpFlag, + 'userType': userType, + 'departmentId': departmentId, + 'departmentName': departmentName, + 'postId': postId, + 'postName': postName, + 'roleId': roleId, + 'email': email, + 'personnelType': personnelType, + 'personnelTypeName': personnelTypeName, + 'nation': nation, + 'nationName': nationName, + 'userIdCard': userIdCard, + 'userAvatarUrl': userAvatarUrl, + 'currentAddress': currentAddress, + 'locationAddress': locationAddress, + 'rankLevel': rankLevel, + 'rankLevelName': rankLevelName, + 'phone': phone, + 'sort': sort, + 'version': version, + 'createId': createId, + 'createName': createName, + 'createTime': createTime, + 'updateId': updateId, + 'updateName': updateName, + 'updateTime': updateTime, + 'remarks': remarks, + 'deleteEnum': deleteEnum, + 'tenantId': tenantId, + 'orgId': orgId, + 'env': env, + 'departmentLeaderFlag': departmentLeaderFlag, + 'deputyLeaderFlag': deputyLeaderFlag, + 'culturalLevel': culturalLevel, + 'culturalLevelName': culturalLevelName, + 'maritalStatus': maritalStatus, + 'maritalStatusName': maritalStatusName, + 'politicalAffiliation': politicalAffiliation, + 'politicalAffiliationName': politicalAffiliationName, + 'mappingName': mappingName, + 'mappingUserName': mappingUserName, + 'mappingPostName': mappingPostName, + 'mappingDeptName': mappingDeptName, + 'employmentFlag': employmentFlag, + }; + } +} + +class SessionService { + SessionService._(); + + static final SessionService instance = SessionService._(); + + // 新接口返回的用户数据 + UserData? userData; + + // 原有的认证相关字段(保留用于兼容性) + String? token; + String? accessTicket; + String? refreshTicket; + String? expireIn; + String? refreshExpiresIn; + + // 自加字段 + String? loginPhone; + String? loginPass; + + // 内部:Prefs key + static const String _prefsKey = 'session_service_v2'; + + // ---------- helpers ---------- + bool get isLoggedIn => token != null && userData?.userId != null && token!.isNotEmpty; + + // 兼容性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; + String? get tenantId => userData?.tenantId; + String? get orgId => userData?.orgId; + String? get departmentId => userData?.departmentId; + String? get departmentName => userData?.departmentName; + String? get roleId => userData?.roleId; + String? get userTypeEnum => userData?.userType?.toString(); + + /// 如果未登录则跳转登录页 + void loginSession(BuildContext context) { + if (!isLoggedIn) { + Navigator.pushReplacementNamed(context, '/login'); + } + } + + /// 清空内存并清除本地存储 + Future clear({bool clearPrefs = true}) async { + userData = null; + token = null; + accessTicket = null; + refreshTicket = null; + expireIn = null; + refreshExpiresIn = null; + loginPhone = null; + loginPass = null; + + if (clearPrefs) { + await clearPrefsData(); + } + } + + // ---------- json serialization ---------- + factory SessionService.fromJson(Map json) { + final s = SessionService._(); + + // 解析用户数据 + if (json['userData'] != null) { + s.userData = UserData.fromJson(json['userData'] as Map); + } + + // 解析认证字段 + s.token = json['token'] as String?; + s.accessTicket = json['accessTicket'] as String?; + s.refreshTicket = json['refreshTicket'] as String?; + s.expireIn = json['expireIn'] as String?; + s.refreshExpiresIn = json['refreshExpiresIn'] as String?; + s.loginPhone = json['loginPhone'] as String?; + s.loginPass = json['loginPass'] as String?; + + return s; + } + + Map toJson() { + return { + 'userData': userData?.toJson(), + 'token': token, + 'accessTicket': accessTicket, + 'refreshTicket': refreshTicket, + 'expireIn': expireIn, + 'refreshExpiresIn': refreshExpiresIn, + 'loginPhone': loginPhone, + 'loginPass': loginPass, + }; + } + + // ---------- update helpers ---------- + + /// 从完整的API响应更新会话数据 + void updateFromApiResponse(Map responseJson) { + if (responseJson['data'] != null) { + userData = UserData.fromJson(responseJson['data'] as Map); + } + // 注意:token可能需要从响应头或其他地方单独设置 + } + + /// 从JSON更新会话数据(兼容旧版本) + void updateFromJson(Map json) { + final newSession = SessionService.fromJson(json); + // 将 newSession 的值复制到单例 instance + final i = SessionService.instance; + i.userData = newSession.userData; + i.token = newSession.token; + i.accessTicket = newSession.accessTicket; + i.refreshTicket = newSession.refreshTicket; + i.expireIn = newSession.expireIn; + i.refreshExpiresIn = newSession.refreshExpiresIn; + i.loginPhone = newSession.loginPhone; + i.loginPass = newSession.loginPass; + } + + // ---------- persistence ---------- + Future saveToPrefs() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = jsonEncode(toJson()); + await prefs.setString(_prefsKey, jsonStr); + } + + Future loadFromPrefs() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_prefsKey); + if (jsonStr == null || jsonStr.isEmpty) return false; + try { + final Map map = jsonDecode(jsonStr) as Map; + updateFromJson(map); + return true; + } catch (_) { + return false; + } + } + + Future clearPrefsData() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_prefsKey); + } + + // ---------- convenience setters ---------- + void setToken(String t) => token = t; + void setLoginPhone(String phone) => loginPhone = phone; + void setLoginPass(String pass) => loginPass = pass; + + // ---------- 打印用户信息(调试用) ---------- + void printUserInfo() { + if (userData != null) { + print('用户ID: ${userData!.userId}'); + print('用户名: ${userData!.username}'); + print('姓名: ${userData!.name}'); + print('部门: ${userData!.departmentName}'); + print('租户ID: ${userData!.tenantId}'); + } + } +} + +// ---------- example usage ---------- +// 在登录成功后: +// SessionService.instance.updateFromApiResponse(responseJson); +// SessionService.instance.setToken('从响应头获取的token'); // 如果需要 +// await SessionService.instance.saveToPrefs(); +// +// 程序启动时尝试恢复: +// await SessionService.instance.loadFromPrefs(); \ No newline at end of file diff --git a/lib/services/StorageService.dart b/lib/services/StorageService.dart new file mode 100644 index 0000000..7d1bfb6 --- /dev/null +++ b/lib/services/StorageService.dart @@ -0,0 +1,52 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class StorageService { + StorageService._internal(); + static final StorageService instance = StorageService._internal(); + + late final SharedPreferences _prefs; + + /// 启动时调用一次,确保 prefs 已就绪 + Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + /// 存储 String + Future setString(String key, String value) => + _prefs.setString(key, value); + + /// 读取 String + String? getString(String key) => _prefs.getString(key); + + /// 存储 String 列表 + Future setStringList(String key, List value) => + _prefs.setStringList(key, value); + + /// 读取 String 列表 + List? getStringList(String key) => _prefs.getStringList(key); + + /// 存储 int + Future setInt(String key, int value) => _prefs.setInt(key, value); + + /// 读取 int + int? getInt(String key) => _prefs.getInt(key); + + /// 存储 bool + Future setBool(String key, bool value) => _prefs.setBool(key, value); + + /// 读取 bool + bool? getBool(String key) => _prefs.getBool(key); + + /// 存储 double + Future setDouble(String key, double value) => + _prefs.setDouble(key, value); + + /// 读取 double + double? getDouble(String key) => _prefs.getDouble(key); + + /// 删除单个 key + Future remove(String key) => _prefs.remove(key); + + /// 清空所有 + Future clear() => _prefs.clear(); +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..4d42eaa --- /dev/null +++ b/lib/services/auth_service.dart @@ -0,0 +1,206 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:qhd_prevention/services/StorageService.dart'; +import 'package:qhd_prevention/tools/encrypt.dart'; +import 'package:qhd_prevention/tools/tools.dart' hide C1C2C3; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../http/ApiService.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; +import 'package:qhd_prevention/services/heartbeat_service.dart'; + +class AuthService { + static const _keyUser = 'USER'; + static const _keyUserInfoData = 'user_info'; + static const _keyRemember = 'remember'; + static const _keyIsLoggedIn = 'isLoggedIn'; + + /// 登录 + static Future> login( + Map formData, + String username, + String password, + ) async { + final data = {'phone': username, 'password': password, ...formData}; + final res = await AuthApi.userlogin(data); + if (!res['success']) { + Fluttertoast.showToast(msg: res['errMessage'] ?? ''); + return {}; + } + final resData = res['data']; + bool isInfoComplete = resData['isInfoComplete'] ?? false; + List firmList = resData['corpInfoCOList'] ?? []; + + if (firmList.length == 1 && isInfoComplete) { + // 入职一个企业直接进行底座登录传入企业 + Map data = { + 'id': firmList.first['id'] ?? '', + 'corpinfoId': firmList.first['corpinfoId'] ?? '', + }; + return AuthService.gbsLogin(username, password, data); + } else if (firmList.length > 1) { + // 如果入职多个 + if (StorageService.instance.getString('key.saveJoinFirmInfo') != null) { + // 有缓存的登录过的企业 + Map jsonData = json.decode( + StorageService.instance.getString('key.saveJoinFirmInfo') ?? '{}', + ); + return AuthService.gbsLogin(username, password, jsonData); + } else { + // 多个企业 也没有缓存过登录的企业 + return { + 'isChooseFirm': false, + 'isInfoComplete': isInfoComplete, + 'firmList': firmList, + 'userName': username, + 'password': password, + }; + } + } else { + return { + 'isChooseFirm': false, + 'isInfoComplete': isInfoComplete, + 'firmList': [], + 'userName': username, + 'password': password, + }; + } + } + + static Future> gbsLogin( + String username, + String password, + Map params, + ) async { + final encrypted = Encrypt.encrypt(password); + if (encrypted == null) { + Fluttertoast.showToast(msg: '加密失败'); + return {}; + } + Map formData = { + 'captcha': "9999", + 'clientId': ApiService.clientId, + 'identifier': '', + 'username': username, + 'captchaModeEnum': 'GRAPH', + 'grantTypeEnum': 'ACCOUNT_MOBILE_PASSWORD', + 'smsTwiceVerification': 'FALSE', + 'userTypeEnum': 'PLATFORM', + ...params, + }; + final _data = Map.from(formData); + + _data['password'] = encrypted; + // printLongString(jsonEncode(_data)); + final result = await AuthApi.loginCheck(_data); + final success = result['success'] as bool; + if (!success) { + Fluttertoast.showToast(msg: result['errMessage'] ?? ''); + return result; + // return false; + } + + printLongString('token:${jsonEncode(result['data']['token'])}'); + final data = result['data'] as Map; + final token = data['token'] ?? ''; + SessionService.instance.setToken(token); + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_keyUser, json.encode(data)); + await prefs.setStringList(_keyRemember, [username, password]); + await prefs.setBool(_keyIsLoggedIn, true); + + /// 保存登录的企业 + StorageService.instance.setString( + 'key.saveJoinFirmInfo', + json.encode(params), + ); + + await AuthApi.getUserData().then((result) async { + SessionService.instance.updateFromApiResponse(result); + await SessionService.instance.saveToPrefs(); + SessionService.instance.setToken(token); + }); + return result; + } + + // 检查并请求相机权限 + static Future checkCameraPermission() async { + final status = await Permission.camera.status; + if (status.isDenied) { + final result = await Permission.camera.request(); + return result.isGranted; + } + return status.isGranted; + } + + // 检查并请求照片库权限(iOS)和存储权限(Android) + static Future checkPhotoLibraryPermission() async { + if (Platform.isIOS) { + final status = await Permission.photos.status; + if (status.isDenied) { + final result = await Permission.photos.request(); + return result.isGranted; + } + return status.isGranted; + } else { + final status = await Permission.storage.status; + if (status.isDenied) { + final result = await Permission.storage.request(); + return result.isGranted; + } + return status.isGranted; + } + } + + /// 验证是否已登录(通过重新登录) + static Future> isLoggedIn() async { + return {}; + // final prefs = await SharedPreferences.getInstance(); + // final isLocalLoggedIn = prefs.getBool(_keyIsLoggedIn) ?? false; + // if (!isLocalLoggedIn) return {}; + // + // final remember = prefs.getStringList(_keyRemember); + // if (remember == null || remember.length < 2) return {}; + // + // final username = remember[0]; + // final password = remember[1]; + // + // // 用存储的账号密码重新登录 + // final data = await login(username, password); + // + // if (data.isEmpty||data['result']!= 'success') { + // await logout(); + // return data; + // } + // return data; + } + + /// 获取已保存的登录信息 + static Future?> getUser() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_keyUser); + if (jsonStr == null) return null; + return json.decode(jsonStr); + } + + /// 获取已保存的用户信息 + static Future?> getUserInfoData() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_keyUserInfoData); + if (jsonStr == null) return null; + return json.decode(jsonStr); + } + + /// 登出 + static Future logout() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_keyUser); + await prefs.remove(_keyRemember); + await prefs.setBool(_keyIsLoggedIn, false); + // 停止心跳服务 + HeartbeatService().stop(); + // 清空Session + await SessionService.instance.clear(); + } +} diff --git a/lib/services/heartbeat_service.dart b/lib/services/heartbeat_service.dart new file mode 100644 index 0000000..efe6884 --- /dev/null +++ b/lib/services/heartbeat_service.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/services/SessionService.dart'; + +/// 心跳服务 - 单例模式 +/// 每5秒发送一次心跳请求 +class HeartbeatService { + HeartbeatService._internal(); + + static final HeartbeatService _instance = HeartbeatService._internal(); + + factory HeartbeatService() => _instance; + + Timer? _heartbeatTimer; + bool _isRunning = false; + bool _isPaused = false; // 用于应用生命周期暂停 + + /// 心跳间隔(秒) + static const Duration heartbeatInterval = Duration(seconds: 5); + + /// 是否正在运行 + bool get isRunning => _isRunning; + + /// 启动心跳 + /// 只有在用户已登录时才会启动 + void start() { + // 检查是否已登录 + if (!SessionService.instance.isLoggedIn) { + debugPrint('HeartbeatService: 用户未登录,不启动心跳'); + return; + } + + // 如果已经在运行,不重复启动 + if (_isRunning && _heartbeatTimer != null) { + debugPrint('HeartbeatService: 心跳已在运行'); + return; + } + + _isRunning = true; + _isPaused = false; + debugPrint('HeartbeatService: 启动心跳,间隔 ${heartbeatInterval.inSeconds} 秒'); + + // 立即发送一次心跳 + _sendHeartbeat(); + + // 启动定时器 + _heartbeatTimer = Timer.periodic(heartbeatInterval, (timer) { + if (!_isPaused && SessionService.instance.isLoggedIn) { + _sendHeartbeat(); + } else if (!SessionService.instance.isLoggedIn) { + // 如果用户已退出登录,停止心跳 + stop(); + } + }); + } + + /// 停止心跳 + void stop() { + if (_heartbeatTimer != null) { + _heartbeatTimer!.cancel(); + _heartbeatTimer = null; + } + _isRunning = false; + _isPaused = false; + debugPrint('HeartbeatService: 停止心跳'); + } + + /// 暂停心跳(应用进入后台时) + void pause() { + if (_isRunning) { + _isPaused = true; + debugPrint('HeartbeatService: 暂停心跳'); + } + } + + /// 恢复心跳(应用回到前台时) + void resume() { + if (_isRunning && _isPaused) { + _isPaused = false; + debugPrint('HeartbeatService: 恢复心跳'); + // 立即发送一次心跳 + _sendHeartbeat(); + } + } + /// 发送心跳请求 + Future _sendHeartbeat() async { + try { + // 检查登录状态 + if (!SessionService.instance.isLoggedIn) { + debugPrint('HeartbeatService: 用户未登录,停止心跳'); + stop(); + return; + } + // 调用心跳接口 + // await HiddenDangerApi.heartbeat(); + // debugPrint('HeartbeatService: 心跳发送成功'); + } catch (e) { + debugPrint('HeartbeatService: 心跳发送失败: $e'); + // 心跳失败不停止服务,继续尝试 + } + } +} + diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart new file mode 100644 index 0000000..61d671d --- /dev/null +++ b/lib/services/location_service.dart @@ -0,0 +1,71 @@ +import 'package:geolocator/geolocator.dart'; + +class LocationResult { + final String latitude; + final String longitude; + + LocationResult({ + required this.latitude, + required this.longitude, + }); + + factory LocationResult.fromPosition(Position p) { + return LocationResult( + latitude: p.latitude.toString(), + longitude: p.longitude.toString(), + ); + } + + double? get latitudeAsDouble => double.tryParse(latitude); + double? get longitudeAsDouble => double.tryParse(longitude); + + @override + String toString() => 'LocationResult(latitude: $latitude, longitude: $longitude)'; +} + +class LocationException implements Exception { + final String message; + LocationException(this.message); + @override + String toString() => message; +} + +/// 统一处理定位权限与获取经纬度(字符串形式) +class LocationService { + /// 获取当前经纬度(字符串形式),成功返回 LocationResult,失败抛出 LocationException + static Future getCurrentLocation({ + Duration timeout = const Duration(seconds: 10), + }) async { + // 检查定位服务是否开启 + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + await Geolocator.openLocationSettings(); + throw LocationException('定位服务未开启,请打开设备定位后重试'); + } + + // 检查/请求权限 + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + throw LocationException('定位权限被拒绝'); + } + } + + if (permission == LocationPermission.deniedForever) { + throw LocationException( + '定位权限被永久拒绝,请去系统设置中为应用打开定位权限', + ); + } + + // 获取位置(带超时) + try { + final position = await Geolocator + .getCurrentPosition(desiredAccuracy: LocationAccuracy.high) + .timeout(timeout); + return LocationResult.fromPosition(position); + } on Exception catch (e) { + throw LocationException('获取位置失败: ${e.toString()}'); + } + } +} diff --git a/lib/services/update_service.dart b/lib/services/update_service.dart new file mode 100644 index 0000000..6462305 --- /dev/null +++ b/lib/services/update_service.dart @@ -0,0 +1,168 @@ +// update_service.dart +import 'dart:async'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +/// 安装 MethodChannel 名称(与原生保持一致) +const String _kInstallChannel = 'app.install'; + +/// 更新事件类型 +enum UpdateState { idle, starting, downloading, completed, installing, installed, failed, canceled } + +class UpdateEvent { + final UpdateState state; + final String? message; // 可选的错误/提示消息 + + UpdateEvent(this.state, {this.message}); +} + +/// 单例服务类:管理下载、进度与安装 +class UpdateService { + UpdateService._internal(); + + static final UpdateService _instance = UpdateService._internal(); + + factory UpdateService() => _instance; + + // 进度通知器(0.0 ~ 1.0) + final ValueNotifier progress = ValueNotifier(0.0); + + // 状态流控制器(广播) + final StreamController _statusController = + StreamController.broadcast(); + + Stream get statusStream => _statusController.stream; + + // Dio cancel token 用于取消下载 + CancelToken? _cancelToken; + + final MethodChannel _channel = const MethodChannel(_kInstallChannel); + + /// 启动下载并在下载完成后尝试安装 + /// apkUrl: 下载地址 + /// apkFileName: 保存的文件名(可选) + Future downloadAndInstall({ + required String apkUrl, + String? apkFileName, + }) async { + // 防止重复下载 + if (_cancelToken != null && !_cancelToken!.isCancelled) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '已有下载进行中')); + return; + } + + _statusController.add(UpdateEvent(UpdateState.starting)); + progress.value = 0.0; + + _cancelToken = CancelToken(); + + // 准备保存路径(app 专属外部目录) + String savePath; + try { + final extDir = await getExternalStorageDirectory(); + if (extDir == null) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '无法获取存储目录')); + return; + } + apkFileName ??= 'app_update_${DateTime.now().millisecondsSinceEpoch}.apk'; + savePath = '${extDir.path}/$apkFileName'; + } catch (e) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '获取存储路径失败: $e')); + return; + } + + final dio = Dio(); + + try { + _statusController.add(UpdateEvent(UpdateState.downloading)); + await dio.download( + apkUrl, + savePath, + cancelToken: _cancelToken, + onReceiveProgress: (received, total) { + if (total > 0) { + final p = received / total; + progress.value = p; + } + }, + options: Options( + responseType: ResponseType.stream, + followRedirects: true, + // 不设置超时(可能大文件下载) + receiveTimeout: Duration(seconds: 0), + headers: {"Accept": "application/vnd.android.package-archive"}, + ), + ); + + // 下载完成 + progress.value = 1.0; + _statusController.add(UpdateEvent(UpdateState.completed)); + + // 延迟让 UI 显示 100% + await Future.delayed(const Duration(milliseconds: 200)); + + // 调用原生安装 + _statusController.add(UpdateEvent(UpdateState.installing)); + try { + final Map args = {'path': savePath}; + await _channel.invokeMethod('installApk', args); + // 成功发起安装(系统会弹出安装界面),我们不能保证用户安装成功,但这里当做已触发安装 + _statusController.add(UpdateEvent(UpdateState.installed)); + } on PlatformException catch (e) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '安装失败: ${e.message}')); + } catch (e) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '安装异常: $e')); + } + } on DioError catch (e) { + if (CancelToken.isCancel(e)) { + progress.value = 0.0; + _statusController.add(UpdateEvent(UpdateState.canceled)); + } else { + progress.value = 0.0; + _statusController.add(UpdateEvent(UpdateState.failed, message: e.message)); + } + } catch (e) { + progress.value = 0.0; + _statusController.add(UpdateEvent(UpdateState.failed, message: e.toString())); + } finally { + // 清理 token + _cancelToken = null; + } + } + + /// 取消当前下载(若有) + void cancelDownload() { + if (_cancelToken != null && !_cancelToken!.isCancelled) { + _cancelToken!.cancel(); + } + } + + /// 仅触发安装(如果你已经手动下载好了文件),传入本地 apk 路径 + Future installApk(String apkPath) async { + try { + _statusController.add(UpdateEvent(UpdateState.installing)); + final Map args = {'path': apkPath}; + await _channel.invokeMethod('installApk', args); + _statusController.add(UpdateEvent(UpdateState.installed)); + } on PlatformException catch (e) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '安装失败: ${e.message}')); + } catch (e) { + _statusController.add(UpdateEvent(UpdateState.failed, message: '安装异常: $e')); + } + } + + /// 释放资源(页面销毁或 app 退出时调用) + Future dispose() async { + try { + _statusController.add(UpdateEvent(UpdateState.idle)); + await _statusController.close(); + } catch (_) {} + try { + progress.dispose(); + } catch (_) {} + } +} diff --git a/lib/services/upload_file_service.dart b/lib/services/upload_file_service.dart new file mode 100644 index 0000000..e517400 --- /dev/null +++ b/lib/services/upload_file_service.dart @@ -0,0 +1,157 @@ +// upload_file_service.dart +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker/image_picker.dart'; + +/// 上传结果 - 简单包装 +class UploadResult { + final String? filePath; + final String? id; + UploadResult({this.filePath, this.id}); +} + +/// 可接受的上传文件项:支持 File、XFile,或已有的 filePath(表示无需重新上传) +class UploadFileItem { + final File? file; + final XFile? xfile; + final String? filePath; // 已有文件的返回路径(老文件) + UploadFileItem({this.file, this.xfile, this.filePath}); +} + +/// UploadFileService: 可被 Provider/Consumer 或直接持有 +class UploadFileService extends ChangeNotifier { + final Dio dio; + final Map uploadPathEnum; // 对应 UPLOAD_FILE_PATH_ENUM + final Set uploadTypeEnum; // 对应 UPLOAD_FILE_TYPE_ENUM 的取值集合 + + bool _loading = false; + bool get loading => _loading; + + UploadFileService({ + Dio? dioInstance, + required this.uploadPathEnum, + required this.uploadTypeEnum, + }) : dio = dioInstance ?? Dio(); + + void _setLoading(bool v) { + _loading = v; + notifyListeners(); + } + + /// options: + /// - files: List + /// - single: bool (default true) + /// - params: Map (must contain 'type') + /// + /// 返回 UploadResult: + /// - single: 返回 filePath 字段 + /// - batch(single==false): 返回 id 字段(对应 foreignKey) + Future uploadFile({ + required List? files, + bool single = true, + required Map? params, + }) async { + if (params == null) { + throw ArgumentError('请传入 options.params'); + } + final type = params['type']; + if (type == null) { + throw ArgumentError('请传入 options.params.type'); + } + if (!uploadTypeEnum.contains(type)) { + throw ArgumentError('传入的 type 不在 UPLOAD_FILE_TYPE_ENUM 中'); + } + + final path = uploadPathEnum[type]; + if (path == null || path.isEmpty) { + throw ArgumentError('未找到 type $type 对应的 path'); + } + if (!single && (params['foreignKey'] == null)) { + throw ArgumentError('single 为 false 时,options.params.foreignKey 必需'); + } + + _setLoading(true); + + try { + final fileItems = files ?? []; + + // 如果没有文件则直接返回(与原逻辑一致) + if (fileItems.isEmpty) { + return single + ? UploadResult(filePath: '') + : UploadResult(id: ''); + } + + // 检查是否有真正需要上传的文件(File 或 XFile) + final needUpload = fileItems.where((it) => it.file != null || it.xfile != null).toList(); + + // 如果没有实际要上传的文件,返回老文件(files[0].filePath 或 params.foreignKey) + if (needUpload.isEmpty) { + _setLoading(false); + return single + ? UploadResult(filePath: fileItems[0].filePath ?? '') + : UploadResult(id: params['foreignKey']?.toString() ?? ''); + } + + // 构建 FormData + final formData = FormData(); + + // 添加文件字段 - 同名 "files"(与后端约定) + for (final it in needUpload) { + String filePath; + if (it.file != null) { + filePath = it.file!.path; + } else if (it.xfile != null) { + // XFile.path 在 web 上不是本地文件(注意),这里只为移动端/桌面可用 + filePath = it.xfile!.path; + } else { + continue; + } + final filename = filePath.split(Platform.pathSeparator).last; + final multipart = await MultipartFile.fromFile(filePath, filename: filename); + formData.files.add(MapEntry('files', multipart)); + } + + // 添加 params 字段(全部作为字符串) + params.forEach((k, v) { + if (v == null) return; + formData.fields.add(MapEntry(k, v.toString())); + }); + + // 添加 path 字段 + formData.fields.add(MapEntry('path', path)); + + // 选择 URL:单文件/批量 + final url = single ? '/basicInfo/imgFiles/save' : '/basicInfo/imgFiles/batchSave'; + + final response = await dio.post( + url, + data: formData, + options: Options( + contentType: 'multipart/form-data', + // 如果需要加额外 headers 可以在这里传入 + ), + ); + + // 解析响应(假定后端返回结构与原 JS 一致:res.data.filePath / res.data.foreignKey) + final resData = response.data; + if (single) { + final String? returnedPath = (resData is Map && resData['filePath'] != null) + ? resData['filePath'].toString() + : null; + return UploadResult(filePath: returnedPath); + } else { + final String? foreignKey = (resData is Map && resData['foreignKey'] != null) + ? resData['foreignKey'].toString() + : null; + return UploadResult(id: foreignKey); + } + } catch (e) { + // 将错误上抛,调用方可 catch + rethrow; + } finally { + _setLoading(false); + } + } +} diff --git a/lib/tools/SmallWidget.dart b/lib/tools/SmallWidget.dart new file mode 100644 index 0000000..8e276b8 --- /dev/null +++ b/lib/tools/SmallWidget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'h_colors.dart'; +/// 标签(eg:风险等级) +Widget riskTagText(int level, String title) { + + final List colors = riskLevelTextColors(); + if (colors.length <= (level - 1)) { + return SizedBox(); + } + return Container( + padding: EdgeInsets.symmetric(vertical: 3, horizontal: 5), + decoration: BoxDecoration( + color: colors[level-1], + borderRadius: const BorderRadius.all(Radius.circular(5)), + ), + // color: Colors., + child: Text( + title, + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ); +} \ No newline at end of file diff --git a/lib/tools/VideoConverter.dart b/lib/tools/VideoConverter.dart new file mode 100644 index 0000000..f1fbb10 --- /dev/null +++ b/lib/tools/VideoConverter.dart @@ -0,0 +1,47 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'package:video_compress/video_compress.dart'; + +class VideoConverter { + /// 将视频转成 mp4 格式(如果本来就是 mp4 则直接返回原路径) + static Future convertToMp4(String inputPath) async { + final ext = path.extension(inputPath).toLowerCase(); + + // 已经是 mp4,直接返回 + if (ext == '.mp4') { + return inputPath; + } + + try { + print('开始转换: $inputPath'); + + // 压缩 + 转换格式(输出文件必然是 mp4) + final MediaInfo? info = await VideoCompress.compressVideo( + inputPath, + quality: VideoQuality.DefaultQuality, // 可调: Low, Medium, High + deleteOrigin: false, // 是否删除原文件 + includeAudio: true, + ); + + if (info == null || info.path == null) { + throw Exception('视频转换失败: $inputPath'); + } + + print('转换完成: ${info.path}'); + return info.path!; + } catch (e) { + print('视频转换出错: $e'); + rethrow; + } + } + + /// 将多个视频批量转换为 mp4 + static Future> convertAllToMp4(List videoPaths) async { + final results = []; + for (final path in videoPaths) { + final newPath = await convertToMp4(path); + results.add(newPath); + } + return results; + } +} diff --git a/lib/tools/coord_convert.dart b/lib/tools/coord_convert.dart new file mode 100644 index 0000000..2f76c9a --- /dev/null +++ b/lib/tools/coord_convert.dart @@ -0,0 +1,298 @@ +// lib/utils/location_helper.dart + +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:qhd_prevention/customWidget/custom_alert_dialog.dart'; +import 'package:qhd_prevention/customWidget/toast_util.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// ============ 常量与坐标转换相关(WGS84/GCJ02/BD09) ============ + +const double _pi = 3.1415926535897932384626; +const double _xPi = _pi * 3000.0 / 180.0; +const double _a = 6378245.0; +const double _ee = 0.006693421622965943; + +/// 判断是否在中国境内(仅中国境内需要偏移处理) +bool _outOfChina(double lat, double lon) { + if (lon < 72.004 || lon > 137.8347) return true; + if (lat < 0.8293 || lat > 55.8271) return true; + return false; +} + +double _transformLat(double x, double y) { + double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x.abs()); + ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; + ret += (20.0 * sin(y * _pi) + 40.0 * sin(y / 3.0 * _pi)) * 2.0 / 3.0; + ret += (160.0 * sin(y / 12.0 * _pi) + 320.0 * sin(y * _pi / 30.0)) * 2.0 / 3.0; + return ret; +} + +double _transformLon(double x, double y) { + double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs()); + ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; + ret += (20.0 * sin(x * _pi) + 40.0 * sin(x / 3.0 * _pi)) * 2.0 / 3.0; + ret += (150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0; + return ret; +} + +/// WGS84 -> GCJ02 +List wgs84ToGcj02(double lat, double lon) { + if (_outOfChina(lat, lon)) return [lat, lon]; + double dLat = _transformLat(lon - 105.0, lat - 35.0); + double dLon = _transformLon(lon - 105.0, lat - 35.0); + double radLat = lat / 180.0 * _pi; + double magic = sin(radLat); + magic = 1 - _ee * magic * magic; + double sqrtMagic = sqrt(magic); + dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi); + dLon = (dLon * 180.0) / ((_a / sqrtMagic) * cos(radLat) * _pi); + double mgLat = lat + dLat; + double mgLon = lon + dLon; + return [mgLat, mgLon]; +} + +/// GCJ02 -> BD09 +List gcj02ToBd09(double lat, double lon) { + double x = lon; + double y = lat; + double z = sqrt(x * x + y * y) + 0.00002 * sin(y * _xPi); + double theta = atan2(y, x) + 0.000003 * cos(x * _xPi); + double bdLon = z * cos(theta) + 0.0065; + double bdLat = z * sin(theta) + 0.006; + return [bdLat, bdLon]; +} + +/// 直接 WGS84 -> BD09(先 WGS84->GCJ02,再 GCJ02->BD09) +List wgs84ToBd09(double lat, double lon) { + final gcj = wgs84ToGcj02(lat, lon); + return gcj02ToBd09(gcj[0], gcj[1]); +} + +/// ============ 定位相关设置 ============ + +/// 定位请求超时时间(可根据需要调整) +const Duration _locationTimeout = Duration(seconds: 10); + +/// ============ 新增:获取 Position(带权限/超时/后备策略) ============ +/// 返回 Position(WGS84) +Future getPositionFromGeolocator({ + LocationAccuracy accuracy = LocationAccuracy.high, +}) async { + // 1. 权限检查与请求(先请求权限) + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) { + // 用户拒绝(不是永久拒绝) + throw Exception('定位权限被用户拒绝'); + } + } + if (permission == LocationPermission.deniedForever) { + // 永久拒绝(需要用户手动到设置开启) + throw Exception('定位权限被永久拒绝'); + } + + // 2. 检查定位服务是否开启(GPS/定位开关) + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + throw Exception('定位服务未开启'); + } + + // 3. 尝试获取当前位置(主方案:getCurrentPosition,带超时) + Position? position; + try { + position = await Geolocator.getCurrentPosition(desiredAccuracy: accuracy) + .timeout(_locationTimeout); + } on TimeoutException catch (_) { + position = null; + } catch (_) { + position = null; + } + + // 4. 后备:尝试 getLastKnownPosition(可能是旧位置) + if (position == null) { + try { + position = await Geolocator.getLastKnownPosition(); + } catch (_) { + position = null; + } + } + // 5. 后备:在 Android 上尝试 forceAndroidLocationManager(某些设备/厂商兼容性问题) + if (position == null) { + try { + position = await Geolocator.getCurrentPosition( + desiredAccuracy: accuracy, + forceAndroidLocationManager: true, + ).timeout(_locationTimeout); + } catch (_) { + position = null; + } + } + + // 6. 最终仍无位置 -> 抛异常 + if (position == null) { + throw Exception('无法获取位置信息,请检查设备定位设置或权限'); + } + + return position; +} + +/// 获取 BD09 坐标(保持向后兼容:仍然返回 [bdLat, bdLon]) +/// 内部改为调用 getPositionFromGeolocator 并转换 +Future> getBd09FromGeolocator({ + LocationAccuracy accuracy = LocationAccuracy.high, +}) async { + final pos = await getPositionFromGeolocator(accuracy: accuracy); + return wgs84ToBd09(pos.latitude, pos.longitude); +} + +/// ============ 错误提示(中文映射) ============ +String _mapExceptionToChineseMessage(Object e) { + final msg = e?.toString() ?? ''; + + if (msg.contains('定位权限被用户拒绝') || msg.contains('Location permissions are denied') || msg.contains('denied')) { + return '定位权限被拒绝,请允许应用获取定位权限。'; + } + if (msg.contains('定位权限被永久拒绝') || msg.contains('deniedForever') || msg.contains('permanently denied')) { + return '定位权限被永久拒绝,请到系统设置手动开启定位权限。'; + } + if (msg.contains('定位服务未开启') || msg.contains('Location services are disabled')) { + return '设备定位功能未开启,请打开系统定位后重试。'; + } + if (msg.contains('无法获取位置信息') || msg.contains('无法获取位置信息')) { + return '无法获取有效定位,请检查网络/GPS并重试(可尝试切换到高精度模式)。'; + } + // 默认返回空字符串,调用方根据空串决定是否显示 + return '定位失败:${msg.replaceAll('Exception: ', '')}'; +} +/// 点位数组计算中心点 +Map geographicCentroid(List points) { + if (points.isEmpty) { + throw ArgumentError('points 不能为空'); + } + + double x = 0.0, y = 0.0, z = 0.0; + double altSum = 0.0; + int altCount = 0; + + for (var p in points) { + final lon = p[0]; + final lat = p[1]; + + final latR = lat * pi / 180.0; + final lonR = lon * pi / 180.0; + + final cx = cos(latR) * cos(lonR); + final cy = cos(latR) * sin(lonR); + final cz = sin(latR); + + x += cx; + y += cy; + z += cz; + + if (p.length > 2 && p[2] != null) { + altSum += p[2]; + altCount++; + } + } + + final cnt = points.length.toDouble(); + x /= cnt; + y /= cnt; + z /= cnt; + + final hyp = sqrt(x * x + y * y); + final centroidLat = atan2(z, hyp) * 180.0 / pi; + final centroidLon = atan2(y, x) * 180.0 / pi; + + final result = { + 'lon': centroidLon, + 'lat': centroidLat, + }; + + if (altCount > 0) { + result['alt'] = altSum / altCount; + } + + return result; +} +/// ============ 主业务方法:获取并保存 BD09(同时处理 UI 提示/引导) ============ +/// 现在会同时保存:WGS84(原始) / GCJ02 / BD09,以及保存时间戳(bd_saved_at) +Future fetchAndSaveBd09(BuildContext context) async { + try { + // 获取原始位置(WGS84) + final Position position = await getPositionFromGeolocator(); + + final double wgsLat = position.latitude; + final double wgsLon = position.longitude; + + // 计算 GCJ02(在中国境内会偏移,否则返回原始) + final gcj = wgs84ToGcj02(wgsLat, wgsLon); + final double gcjLat = gcj[0]; + final double gcjLon = gcj[1]; + + // 计算 BD09 + final bd = gcj02ToBd09(gcjLat, gcjLon); + final double bdLat = bd[0]; + final double bdLon = bd[1]; + + // 保存到 SharedPreferences(同时保存原始 WGS84、GCJ02、BD09,以及保存时间) + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('wgs84_lat', wgsLat.toString()); + await prefs.setString('wgs84_lon', wgsLon.toString()); + await prefs.setString('gcj02_lat', gcjLat.toString()); + await prefs.setString('gcj02_lon', gcjLon.toString()); + await prefs.setString('bd_lat', bdLat.toString()); + await prefs.setString('bd_lon', bdLon.toString()); + await prefs.setInt('bd_saved_at', DateTime.now().millisecondsSinceEpoch); + + // 成功提示(按需开启) + // ToastUtil.showNormal(context, '定位成功:$bdLat, $bdLon'); + } on Exception catch (e) { + final msg = e.toString(); + + // 定位权限被永久拒绝 -> 引导用户打开应用设置 + if (msg.contains('定位权限被永久拒绝') || msg.contains('deniedForever') || msg.contains('permanently denied')) { + final open = await CustomAlertDialog.showConfirm( + context, + title: '定位权限', + content: '定位权限被永久拒绝,需要手动到应用设置开启定位权限,是否现在打开设置?', + cancelText: '取消', + confirmText: '去设置', + ); + if (open == true) { + await Geolocator.openAppSettings(); + } + } + // 定位服务未开启 -> 引导用户打开系统定位设置 + else if (msg.contains('定位服务未开启') || msg.contains('Location services are disabled')) { + final open = await CustomAlertDialog.showConfirm( + context, + title: '打开定位', + content: '检测到设备定位服务未开启,是否打开系统定位设置?', + cancelText: '取消', + confirmText: '去打开', + ); + if (open == true) { + await Geolocator.openLocationSettings(); + } + } + // 其它错误 -> 以 toast 显示中文提示(如果映射为空则显示原错误) + else { + final userMsg = _mapExceptionToChineseMessage(e); + if (userMsg.isNotEmpty) { + // ToastUtil.showError(context, userMsg); + } else { + // ToastUtil.showError(context, '定位失败:${e.toString()}'); + } + } + } catch (e) { + // 捕获任何未预期异常 + // ToastUtil.showError(context, '发生未知错误:${e.toString()}'); + } finally { + // 如需隐藏 loading 可在此处处理 + } +} diff --git a/lib/tools/dataTools.dart b/lib/tools/dataTools.dart new file mode 100644 index 0000000..a38ab5e --- /dev/null +++ b/lib/tools/dataTools.dart @@ -0,0 +1,15 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; + +Uint8List compressJson(Map data) { + final jsonStr = jsonEncode(data); + final utf8Bytes = utf8.encode(jsonStr); + final gzipBytes = GZipCodec().encode(utf8Bytes); + return Uint8List.fromList(gzipBytes); +} + +Map decompressJson(Uint8List bytes) { + final decoded = GZipCodec().decode(bytes); + return jsonDecode(utf8.decode(decoded)); +} diff --git a/lib/tools/encrypt.dart b/lib/tools/encrypt.dart new file mode 100644 index 0000000..e1d7ec0 --- /dev/null +++ b/lib/tools/encrypt.dart @@ -0,0 +1,26 @@ + +import 'package:dart_sm/dart_sm.dart'; +import 'package:qhd_prevention/http/ApiService.dart'; +import 'package:qhd_prevention/tools/tools.dart' hide C1C2C3; + +class Encrypt { + static String? encrypt(String text) { + try { + final md5 = md5Hex(text); + // 尝试用 C1C2C3(Hutool 常见顺序) + final raw = SM2.encrypt(md5, ApiService.publicKey); + + final encrypted = ensureC1Has04(raw); + print('encrypted:$encrypted'); + return encrypted; + + } catch (e) { + return null; + // return false; + } + } + static String decrypt(String codeText) { + final dec = SM2.decrypt(codeText, ApiService.privateKey); + return dec; + } +} \ No newline at end of file diff --git a/lib/tools/h_colors.dart b/lib/tools/h_colors.dart new file mode 100644 index 0000000..64c4f47 --- /dev/null +++ b/lib/tools/h_colors.dart @@ -0,0 +1,14 @@ +import 'dart:ui'; + +import 'package:flutter_baidu_mapapi_base/flutter_baidu_mapapi_base.dart'; + +Color h_backGroundColor() => Color(0xFFF1F1F1); +Color h_AppBarColor() => Color(0xFF1C61FF); + +List riskLevelTextColors() { + return [Color(0xFFE54D42),Color(0xFFF37B1D),Color(0xFFF9BD08),Color(0xFF3281FF)]; +} + +List riskLevelBgColors() { + return [Color(0xFFFADBD9),Color(0xFFFCE6D2),Color(0xFFFDF2CE),Color(0xFFCCE6FF)]; +} diff --git a/lib/tools/id_cart_util.dart b/lib/tools/id_cart_util.dart new file mode 100644 index 0000000..c526a17 --- /dev/null +++ b/lib/tools/id_cart_util.dart @@ -0,0 +1,253 @@ +// utils/id_card_util.dart +import 'dart:core'; + +class IDCardInfo { + final String raw; + final bool isValid; + final String? error; + final String? id18; // 标准化到18位(若输入15位则自动转换) + final String? provinceCode; + final String? province; + final DateTime? birthDate; + final String? birth; // YYYY-MM-DD + final int? age; + final String? gender; // '男' / '女' + final bool checksumValid; + final String? constellation; // 星座 + final String? zodiac; // 生肖 + + IDCardInfo({ + required this.raw, + required this.isValid, + this.error, + this.id18, + this.provinceCode, + this.province, + this.birthDate, + this.birth, + this.age, + this.gender, + required this.checksumValid, + this.constellation, + this.zodiac, + }); + + Map toJson() => { + 'raw': raw, + 'isValid': isValid, + 'error': error, + 'id18': id18, + 'provinceCode': provinceCode, + 'province': province, + 'birth': birth, + 'age': age, + 'gender': gender, + 'checksumValid': checksumValid, + 'constellation': constellation, + 'zodiac': zodiac, + }; + + @override + String toString() => toJson().toString(); +} + +/// 主要调用函数:传入身份证号字符串,返回 IDCardInfo +IDCardInfo parseChineseIDCard(String id) { + final raw = (id ?? '').toString().trim().toUpperCase(); + if (raw.isEmpty) { + return IDCardInfo(raw: raw, isValid: false, error: '空字符串', checksumValid: false); + } + + // 省份码映射(常见) + const provinceMap = { + '11': '北京市', + '12': '天津市', + '13': '河北省', + '14': '山西省', + '15': '内蒙古自治区', + '21': '辽宁省', + '22': '吉林省', + '23': '黑龙江省', + '31': '上海市', + '32': '江苏省', + '33': '浙江省', + '34': '安徽省', + '35': '福建省', + '36': '江西省', + '37': '山东省', + '41': '河南省', + '42': '湖北省', + '43': '湖南省', + '44': '广东省', + '45': '广西壮族自治区', + '46': '海南省', + '50': '重庆市', + '51': '四川省', + '52': '贵州省', + '53': '云南省', + '54': '西藏自治区', + '61': '陕西省', + '62': '甘肃省', + '63': '青海省', + '64': '宁夏回族自治区', + '65': '新疆维吾尔自治区', + '71': '台湾省', + '81': '香港特别行政区', + '82': '澳门特别行政区', + '91': '国外' + }; + + // 校验正则:15 位全数字;18 位前17 数字 + 最后一位数字或 X + final reg15 = RegExp(r'^\d{15}$'); + final reg18 = RegExp(r'^\d{17}[\dX]$'); + + String standardized = raw; + bool convertedFrom15 = false; + if (reg15.hasMatch(raw)) { + // 15 位 -> 转 18 位(在第6位后插入 "19"),然后计算校验位 + final prefix17 = raw.substring(0, 6) + '19' + raw.substring(6); + final checkChar = _calcCheckChar(prefix17); + standardized = prefix17 + checkChar; + convertedFrom15 = true; + } else if (reg18.hasMatch(raw)) { + standardized = raw; + } else { + return IDCardInfo( + raw: raw, + isValid: false, + error: '身份证格式不正确(不是15位或18位)', + checksumValid: false, + ); + } + + // 取出生日期 + final birthStr = standardized.substring(6, 14); // YYYYMMDD + final year = int.tryParse(birthStr.substring(0, 4)); + final month = int.tryParse(birthStr.substring(4, 6)); + final day = int.tryParse(birthStr.substring(6, 8)); + + if (year == null || month == null || day == null) { + return IDCardInfo( + raw: raw, + isValid: false, + error: '无法解析出生日期', + id18: standardized, + checksumValid: _verifyCheck(standardized), + ); + } + + // 校验日期是否真实存在(例如闰年等) + DateTime? birthDate; + try { + birthDate = DateTime(year, month, day); + // 额外检查同一天 + if (birthDate.year != year || birthDate.month != month || birthDate.day != day) { + birthDate = null; + } + } catch (e) { + birthDate = null; + } + + if (birthDate == null) { + return IDCardInfo( + raw: raw, + isValid: false, + error: '出生日期无效', + id18: standardized, + checksumValid: _verifyCheck(standardized), + ); + } + + // 年龄计算(按生日是否已过来算) + final now = DateTime.now(); + int age = now.year - birthDate.year; + if (now.month < birthDate.month || (now.month == birthDate.month && now.day < birthDate.day)) { + age -= 1; + } + + // 性别:第 17 位(索引 16)为序列码的最后一位,奇数男 偶数女 + final seq = standardized.substring(14, 17); // 3 位序列号 + final seqNum = int.tryParse(seq); + final gender = (seqNum != null && seqNum % 2 == 1) ? '男' : '女'; + + // 省份 + final provinceCode = standardized.substring(0, 2); + final province = provinceMap[provinceCode] ?? '未知'; + + // 校验位验证 + final checksumValid = _verifyCheck(standardized); + + // 星座与生肖 + final constellation = _calcConstellation(birthDate.month, birthDate.day); + final zodiac = _calcChineseZodiac(birthDate.year); + + return IDCardInfo( + raw: raw, + isValid: true, + id18: standardized, + provinceCode: provinceCode, + province: province, + birthDate: birthDate, + birth: '${birthDate.year.toString().padLeft(4, '0')}-${birthDate.month.toString().padLeft(2, '0')}-${birthDate.day.toString().padLeft(2, '0')}', + age: age, + gender: gender, + checksumValid: checksumValid, + constellation: constellation, + zodiac: zodiac, + ); +} + +// ---- 辅助函数 ---- + +// 计算 17 位前缀的校验码(返回 '0'-'9' 或 'X') +String _calcCheckChar(String id17) { + // 权重 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; + // 校验码映射 remainder -> char + const checkMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; + + int sum = 0; + for (var i = 0; i < 17; i++) { + final ch = id17[i]; + final n = int.tryParse(ch) ?? 0; + sum += n * weights[i]; + } + final mod = sum % 11; + return checkMap[mod]; +} + +// 验证完整 18 位身份证的校验位是否正确 +bool _verifyCheck(String id18) { + if (id18.length != 18) return false; + final id17 = id18.substring(0, 17); + final expected = _calcCheckChar(id17); + final actual = id18[17].toUpperCase(); + return expected == actual; +} + +// 计算星座(西方) +String _calcConstellation(int month, int day) { + const names = [ + '摩羯座', '水瓶座', '双鱼座', '白羊座', '金牛座', '双子座', + '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座' + ]; + const startDays = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22]; + // 月份从1开始 + final idx = (month - 1); + if (day < startDays[idx]) { + return names[(idx + 11) % 12]; + } else { + return names[idx]; + } +} + +// 计算生肖(中国农历生肖按公历年对照,简化算法) +String _calcChineseZodiac(int year) { + const zodiacs = [ + '鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪' + ]; + // 1900 是鼠年(可以用任意基准) + final idx = (year - 1900) % 12; + final i = idx < 0 ? (idx + 12) % 12 : idx; + return zodiacs[i]; +} diff --git a/lib/tools/platform_utils.dart b/lib/tools/platform_utils.dart new file mode 100644 index 0000000..d7f8658 --- /dev/null +++ b/lib/tools/platform_utils.dart @@ -0,0 +1,33 @@ +// lib/utils/platform_utils.dart +import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; + +/// 简单的跨平台判断工具,避免在 web 上引用 dart:io +class PlatformUtils { + /// 是否在浏览器 + static bool get isWeb => kIsWeb; + + /// 是否为 iOS(注意:在 web 上会返回 false) + static bool get isIOS => !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + /// 是否为 Android(在 web 上会返回 false) + static bool get isAndroid => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + + /// 返回一个简单的操作系统标识字符串(web 会返回 'web') + static String get operatingSystem { + if (kIsWeb) return 'web'; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 'android'; + case TargetPlatform.iOS: + return 'ios'; + case TargetPlatform.macOS: + return 'macos'; + case TargetPlatform.linux: + return 'linux'; + case TargetPlatform.windows: + return 'windows'; + case TargetPlatform.fuchsia: + return 'fuchsia'; + } + } +} diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart new file mode 100644 index 0000000..513df9b --- /dev/null +++ b/lib/tools/tools.dart @@ -0,0 +1,724 @@ +import 'dart:convert'; +import 'dart:math'; +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter/services.dart'; +import 'dart:io'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:crypto/crypto.dart' as crypto; + +int getRandomWithNum(int min, int max) { + if (max < min) { + // 保护性处理:交换或抛错,这里交换 + final tmp = min; + min = max; + max = tmp; + } + final random = Random(); + return random.nextInt(max - min + 1) + min; // 生成 [min, max] 的随机数 +} + +double screenHeight(BuildContext context) { + double screenHeight = MediaQuery.of(context).size.height; + return screenHeight; +} +double screenWidth(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + return screenWidth; +} +Future pushPage(Widget page, BuildContext context) { + return Navigator.push( + context, + MaterialPageRoute(builder: (_) => page), + ); +} + +void presentOpaque(Widget page, BuildContext context) { + Navigator.of(context).push( + PageRouteBuilder( + opaque: false, // 允许下层透出 + barrierColor: Colors.black.withOpacity(0.5), //路由遮罩色 + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); +} + +class FocusHelper { + static final FocusNode _emptyNode = FocusNode(); + /// 延迟一帧后再移交焦点,避免不生效的问题 + static void clearFocus(BuildContext context) { + try{ + WidgetsBinding.instance.addPostFrameCallback((_) async { + FocusScope.of(context).requestFocus(_emptyNode); + await SystemChannels.textInput.invokeMethod('TextInput.hide'); + }); + }catch(w) { + + } + + } +} + +/// 文本样式工具类,返回 Text Widget +class HhTextStyleUtils { + /// 主要标题,返回 Text + /// [text]: 文本内容 + /// [color]: 文本颜色,默认黑色 + /// [fontSize]: 字体大小,默认16.0 + /// [bold]: 是否加粗,默认true + static Text mainTitle( + String text, { + Color color = Colors.black, + double fontSize = 14.0, + bool bold = true, + }) { + return Text( + text, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + ), + ); + } + + static TextStyle secondaryTitleStyle = TextStyle( + color: Colors.black54, + fontSize: 13.0, + ); + + /// 次要标题,返回 Text + /// [text]: 文本内容 + /// [color]: 文本颜色,默认深灰 + /// [fontSize]: 字体大小,默认14.0 + /// [bold]: 是否加粗,默认false + static Text secondaryTitle( + String text, { + Color color = Colors.black54, + double fontSize = 12.0, + bool bold = false, + }) { + return Text( + text, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + ), + ); + } + + /// 小文字,返回 Text + /// [text]: 文本内容 + /// [color]: 文本颜色,默认灰色 + /// [fontSize]: 字体大小,默认12.0 + /// [bold]: 是否加粗,默认false + static Text smallText( + String text, { + Color color = Colors.black54, + double fontSize = 11.0, + bool bold = false, + }) { + return Text( + text, + style: TextStyle( + color: color, + fontSize: fontSize, + fontWeight: bold ? FontWeight.bold : FontWeight.normal, + ), + ); + } +} + +/// 版本信息模型类 +class AppVersionInfo { + final String versionName; // 版本名称(如 1.0.0) + final String buildNumber; // 构建号(如 1) + final String fullVersion; // 完整版本(如 1.0.0+1) + + AppVersionInfo({ + required this.versionName, + required this.buildNumber, + required this.fullVersion, + }); + + @override + String toString() { + return fullVersion; + } +} + +// 获取应用版本信息的方法 +Future getAppVersion() async { + try { + final packageInfo = await PackageInfo.fromPlatform(); + return AppVersionInfo( + versionName: packageInfo.version, + buildNumber: packageInfo.buildNumber, + fullVersion: '${packageInfo.version}+${packageInfo.buildNumber}', + ); + } catch (e) { + // 获取失败时返回默认值 + return AppVersionInfo( + versionName: '1.0.0', + buildNumber: '1', + fullVersion: '1.0.0+0', + ); + } +} + +/// ------------------------------------------------------ +/// 日期格式化 +/// ------------------------------------------------------ +String formatDate(DateTime? date, String fmt) { + if (date == null) return ''; + String twoDigits(int n) => n.toString().padLeft(2, '0'); + + final replacements = { + 'yyyy': date.year.toString(), + 'yy': date.year.toString().substring(2), + 'MM': twoDigits(date.month), + 'M': date.month.toString(), + 'dd': twoDigits(date.day), + 'd': date.day.toString(), + 'hh': twoDigits(date.hour), + 'h': date.hour.toString(), + 'mm': twoDigits(date.minute), + 'm': date.minute.toString(), + 'ss': twoDigits(date.second), + 's': date.second.toString(), + }; + + String result = fmt; + replacements.forEach((key, value) { + result = result.replaceAllMapped(RegExp(key), (_) => value); + }); + return result; +} +/// 把 'yyyy-MM-dd HH:mm'(或 'yyyy-MM-ddTHH:mm')解析为 DateTime,失败返回 null +DateTime? _parseYMdHm(String s) { + if (s.isEmpty) return null; + String t = s.trim(); + + // 只包含日期的情况:yyyy-MM-dd + if (RegExp(r'^\d{4}-\d{2}-\d{2}$').hasMatch(t)) { + return DateTime.tryParse(t); // 默认 00:00:00 + } + + // yyyy-MM-dd HH:mm 或 yyyy-MM-ddTHH:mm + if (RegExp(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}$').hasMatch(t)) { + final iso = t.replaceFirst(' ', 'T') + ':00'; + return DateTime.tryParse(iso); + } + + // yyyy-MM-dd HH:mm:ss 或 yyyy-MM-ddTHH:mm:ss + if (RegExp(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}$').hasMatch(t)) { + return DateTime.tryParse(t.replaceFirst(' ', 'T')); + } + + // 都不匹配,返回 null + return null; +} + + +/// 比较两个 'yyyy-MM-dd HH:mm' 格式字符串 +/// 返回 1 (a>b), 0 (a==b), -1 (a compareYMdHmStrings(a, b) == 1; + +/// 便捷:a 是否 早于 b +bool isBeforeStr(String a, String b) => compareYMdHmStrings(a, b) == -1; +/// 判断传入时间字符串 (yyyy-MM-dd HH:mm) 是否早于当前时间 +bool isBeforeNow(String timeStr) { + final dt = _parseYMdHm(timeStr); + if (dt == null) { + throw FormatException("时间格式错误,期望 'yyyy-MM-dd HH:mm' 或 'yyyy-MM-dd HH:mm:ss'"); + } + return dt.isBefore(DateTime.now()); +} +/// ------------------------------------------------------ +/// 防多次点击 +/// ------------------------------------------------------ +class ClickUtil { + ClickUtil._(); + + static bool _canClick = true; + + /// 调用示例: + /// ClickUtil.noMultipleClicks(() { /* your code */ }); + static void noMultipleClicks(VoidCallback fn, {int delayMs = 2000}) { + if (_canClick) { + _canClick = false; + fn(); + Future.delayed(Duration(milliseconds: delayMs), () { + _canClick = true; + }); + } else { + debugPrint('请稍后点击'); + } + } +} + +void presentPage(BuildContext context, Widget page) { + Navigator.push( + context, + MaterialPageRoute(fullscreenDialog: true, builder: (_) => page), + ); +} +class LoadingDialogHelper { + static Timer? _timer; + + /// 显示加载框(带超时,默认 60 秒) + static void show({String? message, Duration timeout = const Duration(seconds: 60)}) { + // 先清理上一个计时器,避免重复 + _timer?.cancel(); + + if (message != null) { + EasyLoading.show(status: message); + } else { + EasyLoading.show(); + } + + // 设置超时自动隐藏 + _timer = Timer(timeout, () { + // 保护性调用 dismiss(避免访问不存在的 isShow) + try { + EasyLoading.dismiss(); + } catch (e) { + debugPrint('EasyLoading.dismiss error: $e'); + } + _timer?.cancel(); + _timer = null; + }); + } + + /// 隐藏加载框(手动触发) + static void hide() { + // 清理计时器 + _timer?.cancel(); + _timer = null; + + try { + EasyLoading.dismiss(); + } catch (e) { + debugPrint('EasyLoading.dismiss error: $e'); + } + } +} + + +/// 将秒数转换为 “HH:MM:SS” 格式 +String secondsCount(dynamic seconds) { + // 先尝试解析出一个 double 值 + double totalSeconds; + if (seconds == null) { + totalSeconds = 0; + } else if (seconds is num) { + totalSeconds = seconds.toDouble(); + } else { + // seconds 是字符串或其他,尝试 parse + totalSeconds = double.tryParse(seconds.toString()) ?? 0.0; + } + + // 取整秒,向下取整 + final int secs = totalSeconds.floor(); + + final int h = (secs ~/ 3600) % 24; + final int m = (secs ~/ 60) % 60; + final int s = secs % 60; + + // padLeft 保证两位数 + final String hh = h.toString().padLeft(2, '0'); + final String mm = m.toString().padLeft(2, '0'); + final String ss = s.toString().padLeft(2, '0'); + + return '$hh:$mm:$ss'; +} +void printLongString(String text, {int chunkSize = 800}) { + final pattern = RegExp('.{1,$chunkSize}'); // 每 chunkSize 个字符一组 + for (final match in pattern.allMatches(text)) { + print(match.group(0)); + } +} + +/// 表单处理 +class FormUtils { + /// 判断 [data] 中的 [key] 是否存在“有效值”: + /// - key 不存在或值为 null -> false + /// - String:去掉首尾空白后非空 -> true + /// - Iterable / Map:非空 -> true + /// - 其它类型(int、double、bool 等)只要不为 null 就算有值 -> true + static bool hasValue(Map data, String key) { + if (!data.containsKey(key)) return false; + final val = data[key]; + if (val == null) return false; + + if (val is String) { + return val.trim().isNotEmpty; + } + if (val is Iterable || val is Map) { + return val.isNotEmpty; + } + // 数字、布尔等其它非空即可 + return true; + } + /// 在list中根据一个 key,value,找到对应的map + static Map findMapForKeyValue( + List list, + String key, + dynamic value, + ) { + // 保留原有行为:null 或 空字符串 返回空 Map + if (value == null) return {}; + if (value is String && value.isEmpty) return {}; + + for (final item in list) { + if (item is! Map) continue; + final v = item[key]; + + if (v == null) continue; + + // 1) 直接相等(类型相同或可直接比较) + if (v == value) { + return Map.from(item); + } + + // 2) 数字与字符串的交叉比较("123" <-> 123) + if (v is num && value is String) { + final parsed = num.tryParse(value); + if (parsed != null && parsed == v) { + return Map.from(item); + } + } else if (v is String && (value is num)) { + final parsed = num.tryParse(v); + if (parsed != null && parsed == value) { + return Map.from(item); + } + } + + // 3) 最后回退到字符串比较(保险) + if (v.toString() == value.toString()) { + return Map.from(item); + } + } + + return {}; + } + + + +} + +class NoDataWidget { + static Widget show({ + String text = '暂无数据', + }) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/null.png', width: 200,), + Text(text, style: TextStyle(color: Colors.grey)), + ], + ), + ); + } + +} +class NativeOrientation { + static const MethodChannel _channel = MethodChannel('app.orientation'); + + static Future setLandscape() async { + try { + final res = await _channel.invokeMethod('setOrientation', 'landscape'); + return res == true; + } on PlatformException catch (e) { + debugPrint('PlatformException setLandscape: $e'); + return false; + } catch (e) { + debugPrint('Unknown error setLandscape: $e'); + return false; + } + } + + static Future setPortrait() async { + try { + final res = await _channel.invokeMethod('setOrientation', 'portrait'); + return res == true; + } on PlatformException catch (e) { + debugPrint('PlatformException setPortrait: $e'); + return false; + } catch (e) { + debugPrint('Unknown error setPortrait: $e'); + return false; + } + } +} + +class CameraPermissionHelper { + static final ImagePicker _picker = ImagePicker(); + + // 检查并请求相机权限(使用 ImagePicker 触发权限请求) + static Future checkAndRequestCameraPermission() async { + if (Platform.isIOS) { + // 对于 iOS,使用 ImagePicker 触发权限请求 + try { + // 尝试获取图片(但立即取消)来触发权限请求 + final XFile? file = await _picker.pickImage( + source: ImageSource.camera, + maxWidth: 1, // 最小尺寸,减少处理时间 + maxHeight: 1, + imageQuality: 1, + ).timeout(const Duration(milliseconds: 100), onTimeout: () { + return null; // 超时返回 null,避免等待用户操作 + }); + + // 无论是否成功获取文件,权限请求已经被触发 + // 现在检查实际的权限状态 + var status = await Permission.camera.status; + return status.isGranted; + } catch (e) { + // 如果出现错误,回退到直接检查权限状态 + var status = await Permission.camera.status; + return status.isGranted; + } + } else { + // Android 使用标准的权限检查方式 + var status = await Permission.camera.status; + if (status.isDenied) { + status = await Permission.camera.request(); + } + return status.isGranted; + } + } + + // 检查并请求相册权限 + static Future checkAndRequestPhotoPermission() async { + if (Platform.isIOS) { + // 对于 iOS,使用 ImagePicker 触发权限请求 + try { + // 尝试获取图片(但立即取消)来触发权限请求 + final XFile? file = await _picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1, + maxHeight: 1, + imageQuality: 1, + ).timeout(const Duration(milliseconds: 100), onTimeout: () { + return null; + }); + + // 检查实际的权限状态 + var status = await Permission.photos.status; + return status.isGranted; + } catch (e) { + var status = await Permission.photos.status; + return status.isGranted; + } + } else { + // Android 使用标准的权限检查方式 + var status = await Permission.storage.status; + if (status.isDenied) { + status = await Permission.storage.request(); + } + return status.isGranted; + } + } +} + + +Future checkNetworkWifi() async { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.mobile) { + print("当前是移动网络(可能是 2G/3G/4G/5G)"); + return false; + } else if (connectivityResult == ConnectivityResult.wifi) { + return true; + print("当前是 WiFi"); + } else if (connectivityResult == ConnectivityResult.ethernet) { + print("当前是有线网络"); + } else if (connectivityResult == ConnectivityResult.none) { + print("当前无网络连接"); + } + return false; + +} +Future openAppStore() async { + String appId = '6739233192'; + // 优先使用 itms-apps 直接打开 App Store 应用(iOS) + final Uri uri = Uri.parse('itms-apps://itunes.apple.com/app/id$appId'); + // 可选:直接打开写评论页: + // final Uri uri = Uri.parse('itms-apps://itunes.apple.com/app/id$appId?action=write-review'); + + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + exit(0); + } else { + // 回退到 https 链接(在浏览器中打开 App Store 页面) + final Uri webUri = Uri.parse('https://itunes.apple.com/app/id$appId'); + if (await canLaunchUrl(webUri)) { + await launchUrl(webUri, mode: LaunchMode.externalApplication); + } else { + throw 'Could not launch App Store for app id $appId'; + } + exit(0); + } +} + +// utils/sm2_format.dart +const int C1C2C3 = 0; +const int C1C3C2 = 1; + +bool _looksLikeHex(String s) { + final str = s.trim(); + return RegExp(r'^[0-9a-fA-F]+$').hasMatch(str) && str.length % 2 == 0; +} + +/// 确保 SM2 密文的 C1 部分以 "04" 开头(hex 表示)。 +/// - cipherHex: SM2.encrypt 的返回值(hex 字符串) +/// - 返回:如果输入看起来不是 hex,会直接返回原值;否则返回已处理的 hex 字符串。 +String ensureC1Has04(String cipherHex) { + if (cipherHex == null) return cipherHex; + String s = cipherHex.trim(); + if (!_looksLikeHex(s)) return s; + + if (s.startsWith('04')) return s; + + // 最小判断:如果长度足够(至少有 128(hex) 的 C1 + 64(hex) 的 C3) + // 128 hex = 64 bytes (X+Y each 32 bytes), C3 = 32 bytes = 64 hex + if (s.length >= 128 + 64) { + // 直接在开头插 '04',把 c1 从 128->130 hex + return '04' + s; + } + + // 长度不符合常见 SM2 (可能是 base64 / 其他格式),不做改动 + return s; +} + +/// 去掉开头的 04(如果后端需要无 04 的 c1) +String removeC1Prefix04(String cipherHex) { + if (cipherHex == null) return cipherHex; + String s = cipherHex.trim(); + if (!_looksLikeHex(s)) return s; + if (s.startsWith('04') && s.length >= 130) { + return s.substring(2); + } + return s; +} + +/// 尝试把输入视作 hex,并返回 hex 字符(或原样返回) +String normalizeToHexIfPossible(dynamic input) { + if (input == null) return ''; + if (input is String) { + final s = input.trim(); + if (_looksLikeHex(s)) return s; + // 如果是 base64,尝试 decode -> hex + try { + final bytes = base64.decode(s); + return bytesToHex(bytes); + } catch (e) { + return s; + } + } + return input.toString(); +} + +/// 辅助:bytes -> hex +String bytesToHex(List bytes) { + final sb = StringBuffer(); + for (final b in bytes) { + sb.write(b.toRadixString(16).padLeft(2, '0')); + } + return sb.toString(); +} +String normalizeSm2Hex(String hex) { + hex = hex.replaceAll(RegExp(r'\s+'), '').toLowerCase(); + // 如果长度是 216 hex(108 bytes),很可能就是缺 0x04 的情况 + if (hex.length == 216) { + return '04' + hex; // 返回 218 hex(109 bytes) + } + // 否则按原样返回(也可加更多校验) + return hex; +} +String md5Hex(String input) { + final bytes = utf8.encode(input); + final digest = crypto.md5.convert(bytes); + return digest.bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); +} + +/// 56个民族数据字典 +List> nationMapList = [ + {"code": "01", "name": "汉族"}, + {"code": "02", "name": "蒙古族"}, + {"code": "03", "name": "回族"}, + {"code": "04", "name": "藏族"}, + {"code": "05", "name": "维吾尔族"}, + {"code": "06", "name": "苗族"}, + {"code": "07", "name": "彝族"}, + {"code": "08", "name": "壮族"}, + {"code": "09", "name": "布依族"}, + {"code": "10", "name": "朝鲜族"}, + {"code": "11", "name": "满族"}, + {"code": "12", "name": "侗族"}, + {"code": "13", "name": "瑶族"}, + {"code": "14", "name": "白族"}, + {"code": "15", "name": "土家族"}, + {"code": "16", "name": "哈尼族"}, + {"code": "17", "name": "哈萨克族"}, + {"code": "18", "name": "傣族"}, + {"code": "19", "name": "黎族"}, + {"code": "20", "name": "傈僳族"}, + {"code": "21", "name": "佤族"}, + {"code": "22", "name": "畲族"}, + {"code": "23", "name": "高山族"}, + {"code": "24", "name": "拉祜族"}, + {"code": "25", "name": "水族"}, + {"code": "26", "name": "东乡族"}, + {"code": "27", "name": "纳西族"}, + {"code": "28", "name": "景颇族"}, + {"code": "29", "name": "柯尔克孜族"}, + {"code": "30", "name": "土族"}, + {"code": "31", "name": "达斡尔族"}, + {"code": "32", "name": "仫佬族"}, + {"code": "33", "name": "羌族"}, + {"code": "34", "name": "布朗族"}, + {"code": "35", "name": "撒拉族"}, + {"code": "36", "name": "毛南族"}, + {"code": "37", "name": "仡佬族"}, + {"code": "38", "name": "锡伯族"}, + {"code": "39", "name": "阿昌族"}, + {"code": "40", "name": "普米族"}, + {"code": "41", "name": "塔吉克族"}, + {"code": "42", "name": "怒族"}, + {"code": "43", "name": "乌孜别克族"}, + {"code": "44", "name": "俄罗斯族"}, + {"code": "45", "name": "鄂温克族"}, + {"code": "46", "name": "德昂族"}, + {"code": "47", "name": "保安族"}, + {"code": "48", "name": "裕固族"}, + {"code": "49", "name": "京族"}, + {"code": "50", "name": "塔塔尔族"}, + {"code": "51", "name": "独龙族"}, + {"code": "52", "name": "鄂伦春族"}, + {"code": "53", "name": "赫哲族"}, + {"code": "54", "name": "门巴族"}, + {"code": "55", "name": "珞巴族"}, + {"code": "56", "name": "基诺族"} +]; \ No newline at end of file diff --git a/lib/tools/update/update_dialogs.dart b/lib/tools/update/update_dialogs.dart new file mode 100644 index 0000000..f3f371a --- /dev/null +++ b/lib/tools/update/update_dialogs.dart @@ -0,0 +1,158 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:qhd_prevention/services/update_service.dart'; + +/// 显示“发现新版本”确认对话框,点击更新后会弹出下载进度弹窗 +Future showUpdateConfirm(BuildContext context, { + required String apkUrl, + +}) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => DownloadProgressDialog(apkUrl: apkUrl), + ); +} + +/// 下载进度弹窗(会在 initState 里自动开始下载) +class DownloadProgressDialog extends StatefulWidget { + final String apkUrl; + final String? apkFileName; + const DownloadProgressDialog({Key? key, required this.apkUrl, this.apkFileName}) : super(key: key); + + @override + State createState() => _DownloadProgressDialogState(); +} + +class _DownloadProgressDialogState extends State { + final UpdateService _service = UpdateService(); + StreamSubscription? _sub; + double _progress = 0.0; + String _statusText = '准备下载...'; + bool _isWorking = true; // 下载或安装过程中禁止重复操作 + + @override + void initState() { + super.initState(); + + // 监听状态流 + _sub = _service.statusStream.listen((event) { + // 根据事件更新 UI + setState(() { + switch (event.state) { + case UpdateState.idle: + _statusText = '空闲'; + break; + case UpdateState.starting: + _statusText = '准备中...'; + break; + case UpdateState.downloading: + _statusText = '下载中...'; + break; + case UpdateState.completed: + _statusText = '下载完成,正在准备安装...'; + break; + case UpdateState.installing: + _statusText = '发起安装...'; + break; + case UpdateState.installed: + _statusText = '已触发安装'; + break; + case UpdateState.failed: + _statusText = '失败: ${event.message ?? ''}'; + break; + case UpdateState.canceled: + _statusText = '已取消'; + break; + } + }); + + // 根据不同状态决定是否关闭弹窗或提示 + if (event.state == UpdateState.installing || event.state == UpdateState.installed) { + // 已发起安装,关闭弹窗让系统安装界面出现 + if (mounted) Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) Navigator.of(context).pop(); + }); + } else if (event.state == UpdateState.failed) { + // 显示错误并在短时间后自动关闭弹窗 + _isWorking = false; + Future.delayed(const Duration(milliseconds: 800), () { + if (mounted) Navigator.of(context).pop(); + if (mounted) ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('更新失败:${event.message ?? '未知错误'}')), + ); + }); + } else if (event.state == UpdateState.canceled) { + _isWorking = false; + if (mounted) { + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) Navigator.of(context).pop(); + if (mounted) ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('下载已取消')), + ); + }); + } + } + }); + + // 监听进度 ValueNotifier + _service.progress.addListener(_onProgressChanged); + + // 启动下载 + _service.downloadAndInstall(apkUrl: widget.apkUrl, apkFileName: widget.apkFileName); + } + + void _onProgressChanged() { + setState(() { + _progress = _service.progress.value; + }); + } + + @override + void dispose() { + _sub?.cancel(); + _service.progress.removeListener(_onProgressChanged); + super.dispose(); + } + + void _onCancel() { + if (!_isWorking) { + // 如果已经不是工作中,直接关闭 + if (Navigator.of(context).canPop()) Navigator.of(context).pop(); + return; + } + + // 取消下载 + _service.cancelDownload(); + // 标记为非工作中(后续状态回调会关闭并提示) + _isWorking = false; + } + + @override + Widget build(BuildContext context) { + final percent = (_progress * 100).clamp(0.0, 100.0); + return PopScope( + canPop: false, // 禁止物理返回 + child: AlertDialog( + title: const Text('正在更新'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator(value: _progress), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('${percent.toStringAsFixed(1)}%'), + Flexible(child: Text(_statusText, overflow: TextOverflow.ellipsis, textAlign: TextAlign.right)), + ], + ), + ], + ), + actions: [ + + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..bd89aff --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1447 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: eefad89f262a873f38d21e5eec853461737ea074d7c9ede39f3ceb135d201cab + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.11.3" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: "1f1d1ff65223c59018d58bdac5211417c2af60bcb469c9d26f928dd412eb91cf" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.24+3" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.22+6" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.12.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "77e53acb64d9de8917424eeb32b5c7c73572d1e00954bbf54a1e609d79a751a2" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.3.5+1" + change_app_package_name: + dependency: "direct main" + description: + name: change_app_package_name + sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.5.0" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.4" + chewie: + dependency: "direct main" + description: + name: chewie + sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.13.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.3.5+1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.8" + dart_sm: + dependency: "direct main" + description: + name: dart_sm + sha256: "98051cca380155c21dc33aef8425d4877f44e857801fd622914bd07d9c06d3dc" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.1.5" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.7.11" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.1" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + sha256: "99b091ec6891ba0c5331fdc2b502993c7c108f898995739a73c6845d71dad70c" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.0" + extended_image: + dependency: transitive + description: + name: extended_image + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "10.0.1" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.0.1" + extension: + dependency: transitive + description: + name: extension + sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "7872545770c277236fd32b022767576c562ba28366204ff1a5628853cf8f2200" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "10.3.7" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.1" + flutter: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_baidu_mapapi_base: + dependency: "direct main" + description: + name: flutter_baidu_mapapi_base + sha256: c8b372f0862690a438ec12e7978d937d775d71a5027632a149448ac4e52b1259 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.9.5" + flutter_baidu_mapapi_map: + dependency: "direct main" + description: + name: flutter_baidu_mapapi_map + sha256: a4ab01a32ffb76c93bcb9b6629736064ce7f0918c58ea3b8468dcb886326d5fb + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.9.5" + flutter_baidu_mapapi_utils: + dependency: "direct main" + description: + name: flutter_baidu_mapapi_utils + sha256: "8ff1127ed81e3a5ca48dae6c5377adaf601f90a3d4f6f07eebafc0e415f88fbb" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.9.5" + flutter_easyloading: + dependency: "direct main" + description: + name: flutter_easyloading + sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.5" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.14.4" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.7" + flutter_new_badger: + dependency: "direct main" + description: + name: flutter_new_badger + sha256: d3742ace8009663db1ac6ba0377b092f479c35deb33e05514ba05cc0b0a5aaaa + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.33" + flutter_spinkit: + dependency: transitive + description: + name: flutter_spinkit + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "8.2.14" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.5" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.6.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.8.13+10" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "997d100ce1dda5b1ba4085194c5e36c9f8a1fb7987f6a36ab677a344cd2dc986" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.8.13+2" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.20.2" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.9.0" + launcher_name: + dependency: "direct main" + description: + name: launcher_name + sha256: "5fc9a8b8de9e255d5f21effc33632b5620771c9b2310d459a7710b725353b305" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.1.1" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.2" + lpinyin: + dependency: "direct main" + description: + name: lpinyin + sha256: "0bb843363f1f65170efd09fbdfc760c7ec34fc6354f9fcb2f89e74866a0d814a" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.3" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "023a71afb4d7cfb5529d0f2636aa8b43db66257905b9486d702085989769c5f2" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "7.1.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "9.1.0" + open_file: + dependency: "direct main" + description: + name: open_file + sha256: d17e2bddf5b278cb2ae18393d0496aa4f162142ba97d1a9e0c30d476adf99c0e + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.5.10" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: "02996f01e5f6863832068e97f8f3a5ef9b613516db6897f373b43b79849e4d07" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.3" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: "1440b1e37ceb0642208cfeb2c659c6cda27b25187a90635c9d1acb7d0584d324" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.3" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.3" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6192e477f34018ef1ea790c56fffc7302e3bc3efede9e798b934c252c8c105ba" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.5.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.3.0" + pdfx: + dependency: "direct main" + description: + name: pdfx + sha256: "29db9b71d46bf2335e001f91693f2c3fbbf0760e4c2eb596bf4bafab211471c1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.9.2" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "7.0.1" + photo_manager: + dependency: "direct main" + description: + name: photo_manager + sha256: "99355f3b3591a00416cc787bbf7f04510f672d602814e0063bf4dc40603041f0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.8.0" + photo_manager_image_provider: + dependency: transitive + description: + name: photo_manager_image_provider + sha256: b6015b67b32f345f57cf32c126f871bced2501236c405aafaefa885f7c821e4f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.0" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.15.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.8" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.0.3" + provider: + dependency: transitive + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "46a46fd64659eff15f4638bbe19de43f9483f0e0bf024a9fb6b3582064bacc7b" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.17" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.1" + simple_gesture_detector: + dependency: transitive + description: + name: simple_gesture_detector + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.4.0" + table_calendar: + dependency: "direct main" + description: + name: table_calendar + sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.4.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.3.1" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.2.0" + video_compress: + dependency: "direct main" + description: + name: video_compress + sha256: "31bc5cdb9a02ba666456e5e1907393c28e6e0e972980d7d8d619a7beda0d4f20" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.4" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.10.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "3f7ef3fb7b29f510e58f4d56b6ffbc3463b1071f2cf56e10f8d25f5b991ed85b" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.8.21" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "6bced1739cf1f96f03058118adb8ac0dd6f96aa1a1a6e526424ab92fd2a6a77d" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.8.7" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.6.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.4.0" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.3.3" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.13.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "3fcca88ee2ae568807ebd42deed235bb8dd8e62b3e4d5caff67daa6bce062cca" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.10.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: a57b76a081bed3bf3a71a486bdf83642b00f1a7342043d50367cea68f338b1af + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.23.4" + wechat_assets_picker: + dependency: "direct main" + description: + name: wechat_assets_picker + sha256: c307e50394c1e6dfcd5c4701e84efb549fce71444fedcf2e671c50d809b3e2a1 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "9.8.0" + wechat_picker_library: + dependency: transitive + description: + name: wechat_picker_library + sha256: "5cb61b9aa935b60da5b043f8446fbb9c5077419f20ccc4856bf444aec4f44bc1" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.7" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.6.1" + xml2json: + dependency: "direct main" + description: + name: xml2json + sha256: "8a7ae63b76676f083d81287b61f03222952609c0507893bc62e60a4a588a9702" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "6.2.7" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..6c2b8be --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,181 @@ +name: qhd_prevention +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 2.2.2+61 + +environment: + sdk: ^3.7.0 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +##flutter_launcher_icons: +# android: true +# ios: true +# image_path: "assets/images/app-logo.png" +# min_sdk_android: 21 # (可选) Android 最低支持 + +flutter_native_splash: + background_image: assets/images/bg-login.png + android: true + ios: true + web: false + +launcher_name: + default: "秦港相关方" + +dependencies: + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + package_info_plus: ^8.3.0 + shared_preferences: ^2.2.2 # 用于保存登录状态 + # 启动页/图标 + flutter_launcher_icons: ^0.14.4 + flutter_native_splash: ^2.4.6 + #应用名 + launcher_name: ^1.0.2 + #包名 + change_app_package_name: ^1.5.0 + # 扫码 + mobile_scanner: ^7.0.1 + #系统权限 + permission_handler: ^12.0.1 + device_info_plus: ^11.5.0 + # 相册 + image_picker: ^1.1.2 + wechat_assets_picker: ^9.5.1 + photo_manager: ^3.7.1 + file_picker: ^10.3.2 + # 日历 + table_calendar: ^3.2.0 + intl: ^0.20.0 + #图片查看大图 + photo_view: ^0.15.0 + #视频播放器 + video_player: ^2.10.0 + #网络监听 + connectivity_plus: ^6.1.4 + #接口请求 + dio: ^5.8.0+1 + #toast + fluttertoast: ^8.2.12 + #网页页面加载 + webview_flutter: ^4.4.0 + path_provider: ^2.0.1 + + camera: ^0.11.2 + #富文本查看 + flutter_html: ^3.0.0 + #pdf、word查看 + pdfx: ^2.9.2 + # nfc相关 +# nfc_manager: ^4.0.2 +# nfc_manager_ndef: ^1.0.1 + #定位 + geolocator: ^10.0.0 + #打开外部预览app + url_launcher: ^6.0.9 + #虚线框 + dotted_border: ^3.1.0 + #未读角标 + flutter_new_badger: ^1.1.1 + #loading + flutter_easyloading: ^3.0.5 + #视频播放器 + chewie: ^1.12.1 + #百度地图 + flutter_baidu_mapapi_base: 3.9.5 + flutter_baidu_mapapi_map: 3.9.5 + flutter_baidu_mapapi_utils: 3.9.5 + #文件处理 + video_compress: ^3.1.4 + #息屏处理 + wakelock_plus: ^1.3.2 + open_file: ^3.2.1 + #SM2、SM4加解密 + dart_sm: ^0.1.5 + # xml解析 + xml2json: ^6.2.7 + #拼音包 + lpinyin: ^2.0.3 + + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/images/ + - assets/icon-apps/ + - assets/js/ + - assets/map/ + - assets/tabbar/ + + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package