PC\19500 4 тижнів тому
батько
коміт
4a53c40bd6
48 змінених файлів з 5194 додано та 0 видалено
  1. 33 0
      .gitignore
  2. 30 0
      .metadata
  3. 3 0
      CHANGELOG.md
  4. 1 0
      LICENSE
  5. 4 0
      analysis_options.yaml
  6. 9 0
      android/.gitignore
  7. 66 0
      android/build.gradle
  8. 1 0
      android/settings.gradle
  9. 12 0
      android/src/main/AndroidManifest.xml
  10. 38 0
      android/src/main/kotlin/com/example/my_feature_module/MyFeatureModulePlugin.kt
  11. 27 0
      android/src/test/kotlin/com/example/my_feature_module/MyFeatureModulePluginTest.kt
  12. 45 0
      example/.gitignore
  13. 16 0
      example/README.md
  14. 28 0
      example/analysis_options.yaml
  15. 14 0
      example/android/.gitignore
  16. 44 0
      example/android/app/build.gradle.kts
  17. 7 0
      example/android/app/src/debug/AndroidManifest.xml
  18. 54 0
      example/android/app/src/main/AndroidManifest.xml
  19. 5 0
      example/android/app/src/main/kotlin/com/example/my_feature_module_example/MainActivity.kt
  20. 12 0
      example/android/app/src/main/res/drawable-v21/launch_background.xml
  21. 12 0
      example/android/app/src/main/res/drawable/launch_background.xml
  22. BIN
      example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  23. BIN
      example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  24. BIN
      example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  25. BIN
      example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  26. BIN
      example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  27. 18 0
      example/android/app/src/main/res/values-night/styles.xml
  28. 18 0
      example/android/app/src/main/res/values/styles.xml
  29. 7 0
      example/android/app/src/profile/AndroidManifest.xml
  30. 24 0
      example/android/build.gradle.kts
  31. 2 0
      example/android/gradle.properties
  32. 5 0
      example/android/gradle/wrapper/gradle-wrapper.properties
  33. 26 0
      example/android/settings.gradle.kts
  34. 25 0
      example/integration_test/plugin_integration_test.dart
  35. 69 0
      example/lib/main.dart
  36. 616 0
      example/pubspec.lock
  37. 86 0
      example/pubspec.yaml
  38. 27 0
      example/test/widget_test.dart
  39. 21 0
      lib/my_feature_module.dart
  40. 17 0
      lib/my_feature_module_method_channel.dart
  41. 29 0
      lib/my_feature_module_platform_interface.dart
  42. 72 0
      lib/src/models/recognition_result.dart
  43. 821 0
      lib/src/services/http_service.dart
  44. 2126 0
      lib/src/widgets/camera_page.dart
  45. 595 0
      lib/src/widgets/smz_page.dart
  46. 75 0
      pubspec.yaml
  47. 27 0
      test/my_feature_module_method_channel_test.dart
  48. 27 0
      test/my_feature_module_test.dart

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+# 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
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins-dependencies
+/build/
+/coverage/

+ 30 - 0
.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: "66dd93f9a27ffe2a9bfc8297506ce066ff51265f"
+  channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
+      base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
+    - platform: android
+      create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
+      base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f
+
+  # 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'

+ 3 - 0
CHANGELOG.md

@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.

+ 1 - 0
LICENSE

@@ -0,0 +1 @@
+TODO: Add your license here.

+ 4 - 0
analysis_options.yaml

@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 9 - 0
android/.gitignore

@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx

+ 66 - 0
android/build.gradle

@@ -0,0 +1,66 @@
+group = "com.example.my_feature_module"
+version = "1.0-SNAPSHOT"
+
+buildscript {
+    ext.kotlin_version = "2.2.20"
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath("com.android.tools.build:gradle:8.11.1")
+        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+apply plugin: "com.android.library"
+apply plugin: "kotlin-android"
+
+android {
+    namespace = "com.example.my_feature_module"
+
+    compileSdk = 36
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
+    }
+
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_17
+    }
+
+    sourceSets {
+        main.java.srcDirs += "src/main/kotlin"
+        test.java.srcDirs += "src/test/kotlin"
+    }
+
+    defaultConfig {
+        minSdk = 24
+    }
+
+    dependencies {
+        testImplementation("org.jetbrains.kotlin:kotlin-test")
+        testImplementation("org.mockito:mockito-core:5.0.0")
+    }
+
+    testOptions {
+        unitTests.all {
+            useJUnitPlatform()
+
+            testLogging {
+               events "passed", "skipped", "failed", "standardOut", "standardError"
+               outputs.upToDateWhen {false}
+               showStandardStreams = true
+            }
+        }
+    }
+}

+ 1 - 0
android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'my_feature_module'

+ 12 - 0
android/src/main/AndroidManifest.xml

@@ -0,0 +1,12 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.example.my_feature_module">
+  
+  <!-- 相机权限 -->
+  <uses-feature android:name="android.hardware.camera" android:required="false" />
+  <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
+  
+  <uses-permission android:name="android.permission.CAMERA" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
+  <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+</manifest>

+ 38 - 0
android/src/main/kotlin/com/example/my_feature_module/MyFeatureModulePlugin.kt

@@ -0,0 +1,38 @@
+package com.example.my_feature_module
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+
+/** MyFeatureModulePlugin */
+class MyFeatureModulePlugin :
+    FlutterPlugin,
+    MethodCallHandler {
+    // The MethodChannel that will the communication between Flutter and native Android
+    //
+    // This local reference serves to register the plugin with the Flutter Engine and unregister it
+    // when the Flutter Engine is detached from the Activity
+    private lateinit var channel: MethodChannel
+
+    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "my_feature_module")
+        channel.setMethodCallHandler(this)
+    }
+
+    override fun onMethodCall(
+        call: MethodCall,
+        result: Result
+    ) {
+        if (call.method == "getPlatformVersion") {
+            result.success("Android ${android.os.Build.VERSION.RELEASE}")
+        } else {
+            result.notImplemented()
+        }
+    }
+
+    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+        channel.setMethodCallHandler(null)
+    }
+}

+ 27 - 0
android/src/test/kotlin/com/example/my_feature_module/MyFeatureModulePluginTest.kt

@@ -0,0 +1,27 @@
+package com.example.my_feature_module
+
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import org.mockito.Mockito
+import kotlin.test.Test
+
+/*
+ * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
+ *
+ * Once you have built the plugin's example app, you can run these tests from the command
+ * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
+ * you can run them directly from IDEs that support JUnit such as Android Studio.
+ */
+
+internal class MyFeatureModulePluginTest {
+    @Test
+    fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
+        val plugin = MyFeatureModulePlugin()
+
+        val call = MethodCall("getPlatformVersion", null)
+        val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
+        plugin.onMethodCall(call, mockResult)
+
+        Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
+    }
+}

+ 45 - 0
example/.gitignore

@@ -0,0 +1,45 @@
+# 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-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release

+ 16 - 0
example/README.md

@@ -0,0 +1,16 @@
+# my_feature_module_example
+
+Demonstrates how to use the my_feature_module plugin.
+
+## 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.

+ 28 - 0
example/analysis_options.yaml

@@ -0,0 +1,28 @@
+# 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:
+    # 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

+ 14 - 0
example/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

+ 44 - 0
example/android/app/build.gradle.kts

@@ -0,0 +1,44 @@
+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")
+}
+
+android {
+    namespace = "com.example.my_feature_module_example"
+    compileSdk = flutter.compileSdkVersion
+    ndkVersion = flutter.ndkVersion
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
+    }
+
+    kotlinOptions {
+        jvmTarget = JavaVersion.VERSION_17.toString()
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId = "com.example.my_feature_module_example"
+        // You can update the following values to match your application needs.
+        // For more information, see: https://flutter.dev/to/review-gradle-config.
+        minSdk = flutter.minSdkVersion
+        targetSdk = flutter.targetSdkVersion
+        versionCode = flutter.versionCode
+        versionName = flutter.versionName
+    }
+
+    buildTypes {
+        release {
+            // TODO: Add your own signing config for the release build.
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            signingConfig = signingConfigs.getByName("debug")
+        }
+    }
+}
+
+flutter {
+    source = "../.."
+}

+ 7 - 0
example/android/app/src/debug/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 54 - 0
example/android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,54 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- 相机权限 -->
+    <uses-feature android:name="android.hardware.camera" android:required="false" />
+    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
+    
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    
+    <application
+        android:label="舌面诊"
+        android:name="${applicationName}"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:launchMode="singleTop"
+            android:taskAffinity=""
+            android:theme="@style/LaunchTheme"
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:hardwareAccelerated="true"
+            android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.NormalTheme"
+              android:resource="@style/NormalTheme"
+              />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+            android:name="flutterEmbedding"
+            android:value="2" />
+    </application>
+    <!-- Required to query activities that can process text, see:
+         https://developer.android.com/training/package-visibility and
+         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
+
+         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.PROCESS_TEXT"/>
+            <data android:mimeType="text/plain"/>
+        </intent>
+    </queries>
+</manifest>

+ 5 - 0
example/android/app/src/main/kotlin/com/example/my_feature_module_example/MainActivity.kt

@@ -0,0 +1,5 @@
+package com.example.my_feature_module_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()

+ 12 - 0
example/android/app/src/main/res/drawable-v21/launch_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?android:colorBackground" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

+ 12 - 0
example/android/app/src/main/res/drawable/launch_background.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Modify this file to customize your launch splash screen -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@android:color/white" />
+
+    <!-- You can insert your own image assets here -->
+    <!-- <item>
+        <bitmap
+            android:gravity="center"
+            android:src="@mipmap/launch_image" />
+    </item> -->
+</layer-list>

BIN
example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 18 - 0
example/android/app/src/main/res/values-night/styles.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 18 - 0
example/android/app/src/main/res/values/styles.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
+    <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <!-- Show a splash screen on the activity. Automatically removed when
+             the Flutter engine draws its first frame -->
+        <item name="android:windowBackground">@drawable/launch_background</item>
+    </style>
+    <!-- Theme applied to the Android Window as soon as the process has started.
+         This theme determines the color of the Android Window while your
+         Flutter UI initializes, as well as behind your Flutter UI while its
+         running.
+
+         This Theme is only used starting with V2 of Flutter's Android embedding. -->
+    <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
+        <item name="android:windowBackground">?android:colorBackground</item>
+    </style>
+</resources>

+ 7 - 0
example/android/app/src/profile/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- The INTERNET permission is required for development. Specifically,
+         the Flutter tool needs it to communicate with the running application
+         to allow setting breakpoints, to provide hot reload, etc.
+    -->
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 24 - 0
example/android/build.gradle.kts

@@ -0,0 +1,24 @@
+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<Delete>("clean") {
+    delete(rootProject.layout.buildDirectory)
+}

+ 2 - 0
example/android/gradle.properties

@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true

+ 5 - 0
example/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

+ 26 - 0
example/android/settings.gradle.kts

@@ -0,0 +1,26 @@
+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()
+    }
+}
+
+plugins {
+    id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+    id("com.android.application") version "8.11.1" apply false
+    id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+}
+
+include(":app")

+ 25 - 0
example/integration_test/plugin_integration_test.dart

@@ -0,0 +1,25 @@
+// This is a basic Flutter integration test.
+//
+// Since integration tests run in a full Flutter application, they can interact
+// with the host side of a plugin implementation, unlike Dart unit tests.
+//
+// For more information about Flutter integration tests, please see
+// https://flutter.dev/to/integration-testing
+
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'package:my_feature_module/my_feature_module.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  testWidgets('getPlatformVersion test', (WidgetTester tester) async {
+    final MyFeatureModule plugin = MyFeatureModule();
+    final String? version = await plugin.getPlatformVersion();
+    // The version string depends on the host platform running the test, so
+    // just assert that some non-empty string is returned.
+    expect(version?.isNotEmpty, true);
+  });
+}

+ 69 - 0
example/lib/main.dart

@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:my_feature_module/my_feature_module.dart';
+
+void main() {
+  runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+  const MyApp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: '舌面诊',
+      theme: ThemeData(
+        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
+        useMaterial3: true,
+      ),
+      home: const HomePage(),
+    );
+  }
+}
+
+class HomePage extends StatelessWidget {
+  const HomePage({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('舌面诊'),
+        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            const Text(
+              '舌面诊',
+              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
+            ),
+            const SizedBox(height: 32),
+            ElevatedButton(
+              onPressed: () {
+                Navigator.of(context).push(
+                  MaterialPageRoute(
+                    builder: (context) => const SmzPage(),
+                  ),
+                );
+              },
+              style: ElevatedButton.styleFrom(
+                padding: const EdgeInsets.symmetric(
+                  horizontal: 32,
+                  vertical: 16,
+                ),
+                backgroundColor: Colors.green,
+                foregroundColor: Colors.white,
+              ),
+              child: const Text(
+                '进入舌面诊',
+                style: TextStyle(fontSize: 18),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 616 - 0
example/pubspec.lock

@@ -0,0 +1,616 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  archive:
+    dependency: transitive
+    description:
+      name: archive
+      sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.0.7"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.13.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  camera:
+    dependency: transitive
+    description:
+      name: camera
+      sha256: dfa8fc5a1adaeb95e7a54d86a5bd56f4bb0e035515354c8ac6d262e35cec2ec8
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.6"
+  camera_android:
+    dependency: transitive
+    description:
+      name: camera_android
+      sha256: "50c0d1c4b122163e3d7cdfcd6d4cd8078aac27d0f1cd1e7b3fa69e6b3f06f4b7"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.10.10+14"
+  camera_avfoundation:
+    dependency: transitive
+    description:
+      name: camera_avfoundation
+      sha256: "035b90c1e33c2efad7548f402572078f6e514d4f82be0a315cd6c6af7e855aa8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.22+6"
+  camera_platform_interface:
+    dependency: transitive
+    description:
+      name: camera_platform_interface
+      sha256: "98cfc9357e04bad617671b4c1f78a597f25f08003089dd94050709ae54effc63"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.12.0"
+  camera_web:
+    dependency: transitive
+    description:
+      name: camera_web
+      sha256: "3bc7bb1657a0f29c34116453c5d5e528c23efcf5e75aac0a3387cf108040bf65"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.5+2"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.2"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.1"
+  cross_file:
+    dependency: transitive
+    description:
+      name: cross_file
+      sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.5+1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.7"
+  cupertino_icons:
+    dependency: "direct main"
+    description:
+      name: cupertino_icons
+      sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.0.8"
+  dio:
+    dependency: transitive
+    description:
+      name: dio
+      sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.9.0"
+  dio_web_adapter:
+    dependency: transitive
+    description:
+      name: dio_web_adapter
+      sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.3"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
+  file_selector_linux:
+    dependency: transitive
+    description:
+      name: file_selector_linux
+      sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.4"
+  file_selector_macos:
+    dependency: transitive
+    description:
+      name: file_selector_macos
+      sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.5"
+  file_selector_platform_interface:
+    dependency: transitive
+    description:
+      name: file_selector_platform_interface
+      sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.7.0"
+  file_selector_windows:
+    dependency: transitive
+    description:
+      name: file_selector_windows
+      sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.9.3+5"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_driver:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.0"
+  flutter_plugin_android_lifecycle:
+    dependency: transitive
+    description:
+      name: flutter_plugin_android_lifecycle
+      sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.33"
+  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"
+  fuchsia_remote_debug_protocol:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.6.0"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.1.2"
+  image:
+    dependency: transitive
+    description:
+      name: image
+      sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.6.0"
+  image_picker:
+    dependency: transitive
+    description:
+      name: image_picker
+      sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  image_picker_android:
+    dependency: transitive
+    description:
+      name: image_picker_android
+      sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.13+10"
+  image_picker_for_web:
+    dependency: transitive
+    description:
+      name: image_picker_for_web
+      sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.1"
+  image_picker_ios:
+    dependency: transitive
+    description:
+      name: image_picker_ios
+      sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.8.13+3"
+  image_picker_linux:
+    dependency: transitive
+    description:
+      name: image_picker_linux
+      sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2"
+  image_picker_macos:
+    dependency: transitive
+    description:
+      name: image_picker_macos
+      sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2+1"
+  image_picker_platform_interface:
+    dependency: transitive
+    description:
+      name: image_picker_platform_interface
+      sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.1"
+  image_picker_windows:
+    dependency: transitive
+    description:
+      name: image_picker_windows
+      sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.2.2"
+  integration_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  leak_tracker:
+    dependency: transitive
+    description:
+      name: leak_tracker
+      sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+      url: "https://pub.dev"
+    source: hosted
+    version: "11.0.2"
+  leak_tracker_flutter_testing:
+    dependency: transitive
+    description:
+      name: leak_tracker_flutter_testing
+      sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.10"
+  leak_tracker_testing:
+    dependency: transitive
+    description:
+      name: leak_tracker_testing
+      sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.2"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.0"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.17"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.11.1"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.17.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
+  my_feature_module:
+    dependency: "direct main"
+    description:
+      path: ".."
+      relative: true
+    source: path
+    version: "0.0.1"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.1"
+  path_provider:
+    dependency: transitive
+    description:
+      name: path_provider
+      sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.5"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.22"
+  path_provider_foundation:
+    dependency: transitive
+    description:
+      name: path_provider_foundation
+      sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.5.1"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.0"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.6"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.8"
+  posix:
+    dependency: transitive
+    description:
+      name: posix
+      sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.0.3"
+  process:
+    dependency: transitive
+    description:
+      name: process
+      sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
+      url: "https://pub.dev"
+    source: hosted
+    version: "5.0.5"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.1"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.12.1"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.1"
+  sync_http:
+    dependency: transitive
+    description:
+      name: sync_http
+      sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.3.1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.2"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.7"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.0"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
+      url: "https://pub.dev"
+    source: hosted
+    version: "15.0.2"
+  web:
+    dependency: transitive
+    description:
+      name: web
+      sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  webdriver:
+    dependency: transitive
+    description:
+      name: webdriver
+      sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.0"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.0"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.6.1"
+sdks:
+  dart: ">=3.9.0 <4.0.0"
+  flutter: ">=3.35.0"

+ 86 - 0
example/pubspec.yaml

@@ -0,0 +1,86 @@
+name: my_feature_module_example
+description: "Demonstrates how to use the my_feature_module plugin."
+# 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
+
+environment:
+  sdk: '>=3.0.0'
+
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+  flutter:
+    sdk: flutter
+
+  my_feature_module:
+    # When depending on this package from a real application you should use:
+    #   my_feature_module: ^x.y.z
+    # See https://dart.dev/tools/pub/dependencies#version-constraints
+    # The example app is bundled with the plugin so we use a path dependency on
+    # the parent directory to use the current plugin's version.
+    path: ../
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^1.0.8
+
+dev_dependencies:
+  integration_test:
+    sdk: flutter
+  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: ^6.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:
+  #   - 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

+ 27 - 0
example/test/widget_test.dart

@@ -0,0 +1,27 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:my_feature_module_example/main.dart';
+
+void main() {
+  testWidgets('Verify Platform version', (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    await tester.pumpWidget(const MyApp());
+
+    // Verify that platform version is retrieved.
+    expect(
+      find.byWidgetPredicate(
+        (Widget widget) => widget is Text &&
+                           widget.data!.startsWith('Running on:'),
+      ),
+      findsOneWidget,
+    );
+  });
+}

+ 21 - 0
lib/my_feature_module.dart

@@ -0,0 +1,21 @@
+library my_feature_module;
+
+// 导出平台接口(保持向后兼容)
+export 'my_feature_module_platform_interface.dart';
+export 'my_feature_module_method_channel.dart';
+
+// 导出主要组件
+export 'src/widgets/smz_page.dart';
+export 'src/widgets/camera_page.dart';
+export 'src/models/recognition_result.dart';
+export 'src/services/http_service.dart';
+
+import 'my_feature_module_platform_interface.dart';
+
+/// MyFeatureModule 主类(保持向后兼容)
+class MyFeatureModule {
+  /// 获取平台版本
+  Future<String?> getPlatformVersion() {
+    return MyFeatureModulePlatform.instance.getPlatformVersion();
+  }
+}

+ 17 - 0
lib/my_feature_module_method_channel.dart

@@ -0,0 +1,17 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'my_feature_module_platform_interface.dart';
+
+/// An implementation of [MyFeatureModulePlatform] that uses method channels.
+class MethodChannelMyFeatureModule extends MyFeatureModulePlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  final methodChannel = const MethodChannel('my_feature_module');
+
+  @override
+  Future<String?> getPlatformVersion() async {
+    final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
+    return version;
+  }
+}

+ 29 - 0
lib/my_feature_module_platform_interface.dart

@@ -0,0 +1,29 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'my_feature_module_method_channel.dart';
+
+abstract class MyFeatureModulePlatform extends PlatformInterface {
+  /// Constructs a MyFeatureModulePlatform.
+  MyFeatureModulePlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static MyFeatureModulePlatform _instance = MethodChannelMyFeatureModule();
+
+  /// The default instance of [MyFeatureModulePlatform] to use.
+  ///
+  /// Defaults to [MethodChannelMyFeatureModule].
+  static MyFeatureModulePlatform get instance => _instance;
+
+  /// Platform-specific implementations should set this with their own
+  /// platform-specific class that extends [MyFeatureModulePlatform] when
+  /// they register themselves.
+  static set instance(MyFeatureModulePlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  Future<String?> getPlatformVersion() {
+    throw UnimplementedError('platformVersion() has not been implemented.');
+  }
+}

+ 72 - 0
lib/src/models/recognition_result.dart

@@ -0,0 +1,72 @@
+/// 识别结果数据模型
+class RecognitionResult {
+  /// 是否识别成功
+  final bool success;
+  
+  /// 识别结果文本列表(如:舌色红、舌苔黄等)
+  final List<String> results;
+  
+  /// 错误信息(识别失败时)
+  final String? errorMessage;
+
+  RecognitionResult({
+    required this.success,
+    this.results = const [],
+    this.errorMessage,
+  });
+
+  /// 从 JSON 创建
+  factory RecognitionResult.fromJson(Map<String, dynamic> json) {
+    return RecognitionResult(
+      success: json['success'] ?? false,
+      results: json['results'] != null 
+          ? List<String>.from(json['results'])
+          : [],
+      errorMessage: json['errorMessage'],
+    );
+  }
+
+  /// 转换为 JSON
+  Map<String, dynamic> toJson() {
+    return {
+      'success': success,
+      'results': results,
+      'errorMessage': errorMessage,
+    };
+  }
+}
+
+/// 图片数据模型
+class ImageData {
+  /// 图片路径
+  final String? imagePath;
+  
+  /// 识别结果
+  final RecognitionResult? recognitionResult;
+  
+  /// 是否显示示例图
+  final bool showExample;
+
+  ImageData({
+    this.imagePath,
+    this.recognitionResult,
+    this.showExample = true,
+  });
+
+  /// 是否有图片
+  bool get hasImage => imagePath != null && imagePath!.isNotEmpty;
+
+  /// 复制并更新
+  ImageData copyWith({
+    String? imagePath,
+    RecognitionResult? recognitionResult,
+    bool? showExample,
+  }) {
+    return ImageData(
+      imagePath: imagePath ?? this.imagePath,
+      recognitionResult: recognitionResult ?? this.recognitionResult,
+      showExample: showExample ?? this.showExample,
+    );
+  }
+}
+

+ 821 - 0
lib/src/services/http_service.dart

@@ -0,0 +1,821 @@
+import 'dart:convert';
+import 'dart:developer' as developer;
+import 'package:dio/dio.dart';
+import 'package:my_feature_module/src/models/recognition_result.dart';
+
+/// HTTP 服务封装类
+class HttpService {
+  static final HttpService _instance = HttpService._internal();
+  factory HttpService() => _instance;
+  HttpService._internal();
+
+  late Dio _dio;
+  String? _baseUrl;
+
+  /// 初始化
+  void init({
+    String? baseUrl,
+    Map<String, dynamic>? headers,
+    int? connectTimeout,
+    int? receiveTimeout,
+  }) {
+    _baseUrl = baseUrl;
+    _dio = Dio(
+      BaseOptions(
+        baseUrl: baseUrl ?? '',
+        headers: headers ?? {},
+        connectTimeout: Duration(milliseconds: connectTimeout ?? 30000),
+        receiveTimeout: Duration(milliseconds: receiveTimeout ?? 30000),
+      ),
+    );
+
+    // 添加拦截器
+    _dio.interceptors.add(LogInterceptor(
+      requestBody: true,
+      responseBody: true,
+    ));
+  }
+
+  /// GET 请求
+  Future<Response<T>> get<T>(
+    String path, {
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+  }) async {
+    try {
+      return await _dio.get<T>(
+        path,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+      );
+    } catch (e) {
+      throw _handleError(e);
+    }
+  }
+
+  /// POST 请求
+  Future<Response<T>> post<T>(
+    String path, {
+    dynamic data,
+    Map<String, dynamic>? queryParameters,
+    Options? options,
+    CancelToken? cancelToken,
+  }) async {
+    try {
+      return await _dio.post<T>(
+        path,
+        data: data,
+        queryParameters: queryParameters,
+        options: options,
+        cancelToken: cancelToken,
+      );
+    } catch (e) {
+      throw _handleError(e);
+    }
+  }
+
+  /// 上传文件
+  Future<Response<T>> uploadFile<T>(
+    String path,
+    String filePath, {
+    String fileKey = 'file',
+    Map<String, dynamic>? data,
+    Options? options,
+    ProgressCallback? onSendProgress,
+    CancelToken? cancelToken,
+  }) async {
+    try {
+      String fileName = filePath.split('/').last;
+      FormData formData = FormData.fromMap({
+        fileKey: await MultipartFile.fromFile(
+          filePath,
+          filename: fileName,
+        ),
+        if (data != null) ...data,
+      });
+
+      return await _dio.post<T>(
+        path,
+        data: formData,
+        options: options,
+        onSendProgress: onSendProgress,
+        cancelToken: cancelToken,
+      );
+    } catch (e) {
+      throw _handleError(e);
+    }
+  }
+
+  /// 上传图片到服务器
+  /// [imagePath] 本地图片路径
+  /// [businessType] 业务类型,默认为 'mini_tongue'
+  /// [token] 认证 token(可选,但通常需要)
+  /// [uploadBaseUrl] 上传接口的基础 URL(可选,如果不提供则使用 _baseUrl)
+  /// 注意:根据小程序代码,上传接口路径是 /api/his-system/file/
+  /// 返回图片 URL
+  Future<String?> uploadImage(
+    String imagePath, {
+    String? businessType,
+    String? token,
+    String? uploadBaseUrl,
+  }) async {
+    try {
+      developer.log(
+        '开始上传图片',
+        name: 'HttpService',
+        error: {
+          'imagePath': imagePath,
+          'businessType': businessType,
+          'hasToken': token != null,
+          'uploadBaseUrl': uploadBaseUrl,
+          'baseUrl': _baseUrl,
+        },
+      );
+
+      // 构建请求头 - BUSINESS-TYPE 应该放在请求头中,而不是 FormData
+      Map<String, String> headers = {};
+      
+      if (businessType != null) {
+        headers['BUSINESS-TYPE'] = businessType;
+      }
+      
+      if (token != null) {
+        headers['Authorization'] = 'Bearer $token';
+      }
+      
+      // 确定上传接口的完整 URL
+      // 上传接口路径是 /api/his-system/file/
+      // 注意:不同于识别接口的服务器
+      // 如果 uploadBaseUrl 未提供,尝试使用 _baseUrl,如果都没有则使用 tongueApiBaseUrl
+      final String finalBaseUrl = uploadBaseUrl ?? _baseUrl ?? '';
+      
+      // 上传接口路径
+      String uploadPath = '/api/his-system/file/';
+      
+      // 如果 finalBaseUrl 为空,说明没有配置上传接口的 baseUrl
+      if (finalBaseUrl.isEmpty) {
+        developer.log(
+          '警告:上传接口 baseUrl 未配置',
+          name: 'HttpService',
+          error: {
+            'uploadBaseUrl': uploadBaseUrl,
+            '_baseUrl': _baseUrl,
+          },
+        );
+      }
+      
+      developer.log(
+        '上传图片请求',
+        name: 'HttpService',
+        error: {
+          'baseUrl': finalBaseUrl,
+          'uploadPath': uploadPath,
+          'fullUrl': '$finalBaseUrl$uploadPath',
+          'filePath': imagePath,
+          'headers': headers,
+          'hasToken': token != null && token.isNotEmpty,
+        },
+      );
+
+      // 如果提供了 uploadBaseUrl,使用新的 Dio 实例;否则使用现有的 _dio
+      Dio dioToUse;
+      if (uploadBaseUrl != null && uploadBaseUrl != _baseUrl) {
+        dioToUse = Dio(BaseOptions(
+          baseUrl: uploadBaseUrl,
+          connectTimeout: const Duration(milliseconds: 60000), // 增加超时时间
+          receiveTimeout: const Duration(milliseconds: 60000),
+          sendTimeout: const Duration(milliseconds: 60000),
+          headers: {
+            'Accept': 'application/json',
+          },
+        ));
+        dioToUse.interceptors.add(LogInterceptor(
+          requestBody: true,
+          responseBody: true,
+          requestHeader: true,
+          responseHeader: true,
+        ));
+      } else {
+        dioToUse = _dio;
+      }
+
+      String fileName = imagePath.split('/').last;
+      FormData formData = FormData.fromMap({
+        'file': await MultipartFile.fromFile(
+          imagePath,
+          filename: fileName,
+        ),
+      });
+
+      final response = await dioToUse.post(
+        uploadPath,
+        data: formData,
+        options: Options(
+          headers: headers,
+          followRedirects: true,
+          validateStatus: (status) => status! < 500, // 允许 4xx 状态码,以便获取错误信息
+        ),
+      );
+      
+      developer.log(
+        '上传图片响应',
+        name: 'HttpService',
+        error: {
+          'statusCode': response.statusCode,
+          'responseType': response.data.runtimeType.toString(),
+          'responseData': response.data is String 
+              ? (response.data as String).substring(0, (response.data as String).length > 200 ? 200 : (response.data as String).length)
+              : response.data,
+        },
+      );
+      
+      // 检查状态码
+      if (response.statusCode == 404) {
+        developer.log(
+          '上传接口不存在(404),请检查接口路径',
+          name: 'HttpService',
+          error: {
+            'uploadPath': uploadPath,
+            'baseUrl': finalBaseUrl,
+            'fullUrl': '$finalBaseUrl$uploadPath',
+          },
+        );
+        throw Exception('上传接口不存在(404),请检查接口路径配置');
+      }
+      
+      if (response.statusCode != null && response.statusCode! >= 400) {
+        developer.log(
+          '上传失败:服务器返回错误状态码',
+          name: 'HttpService',
+          error: {
+            'statusCode': response.statusCode,
+            'responseData': response.data,
+          },
+        );
+        throw Exception('上传失败:服务器错误 ${response.statusCode}');
+      }
+      
+      // 处理响应数据:可能是 String 或 Map
+      Map<String, dynamic>? responseData;
+      if (response.data is String) {
+        final String jsonString = response.data as String;
+        // 如果是 HTML(通常是错误页面),跳过解析
+        if (jsonString.trim().startsWith('<!DOCTYPE') || jsonString.trim().startsWith('<html')) {
+          developer.log(
+            '服务器返回 HTML 页面(可能是错误页面)',
+            name: 'HttpService',
+            error: {'statusCode': response.statusCode},
+          );
+          throw Exception('上传失败:服务器返回错误页面(${response.statusCode})');
+        }
+        
+        try {
+          if (jsonString.trim().isNotEmpty) {
+            responseData = jsonDecode(jsonString) as Map<String, dynamic>?;
+            developer.log(
+              '成功解析 JSON 字符串',
+              name: 'HttpService',
+              error: {'responseData': responseData},
+            );
+          }
+        } catch (e) {
+          developer.log(
+            '解析 JSON 字符串失败',
+            name: 'HttpService',
+            error: {'error': e, 'jsonString': jsonString.substring(0, jsonString.length > 100 ? 100 : jsonString.length)},
+          );
+          throw Exception('上传失败:服务器响应格式错误');
+        }
+      } else if (response.data is Map) {
+        responseData = response.data as Map<String, dynamic>?;
+      }
+      
+      if (responseData != null && 
+          responseData['success'] == true && 
+          responseData['data'] != null) {
+        final data = responseData['data'] as Map<String, dynamic>;
+        final imageUrl = data['url'] as String?;
+        developer.log(
+          '上传图片成功',
+          name: 'HttpService',
+          error: {'imageUrl': imageUrl},
+        );
+        return imageUrl;
+      }
+      
+      developer.log(
+        '上传图片失败:响应数据格式错误',
+        name: 'HttpService',
+        error: {'responseData': responseData ?? response.data},
+      );
+      return null;
+    } catch (e, stackTrace) {
+      developer.log(
+        '上传图片异常',
+        name: 'HttpService',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      
+      // 如果是 DioException,提供更详细的错误信息
+      if (e is DioException) {
+        String errorMsg = '上传图片失败';
+        if (e.type == DioExceptionType.connectionTimeout) {
+          errorMsg = '连接超时,请检查网络';
+        } else if (e.type == DioExceptionType.receiveTimeout) {
+          errorMsg = '接收超时,请稍后重试';
+        } else if (e.type == DioExceptionType.sendTimeout) {
+          errorMsg = '发送超时,请检查网络';
+        } else if (e.response != null) {
+          errorMsg = '服务器错误: ${e.response?.statusCode}';
+          if (e.response?.data != null) {
+            developer.log(
+              '服务器响应错误',
+              name: 'HttpService',
+              error: e.response?.data,
+            );
+          }
+        } else if (e.type == DioExceptionType.connectionError) {
+          // DNS 解析失败或连接错误
+          final errorString = e.error?.toString() ?? '';
+          if (errorString.contains('Failed host lookup') || 
+              errorString.contains('No address associated with hostname')) {
+            errorMsg = '无法连接到服务器,请检查域名配置或网络连接';
+            developer.log(
+              'DNS 解析失败或连接错误',
+              name: 'HttpService',
+              error: {
+                'error': e.error,
+                'baseUrl': uploadBaseUrl ?? _baseUrl,
+                'message': '请确认域名是否正确,或检查网络连接',
+              },
+            );
+          } else {
+            errorMsg = '网络连接错误: ${e.error}';
+          }
+        } else if (e.error != null) {
+          errorMsg = '网络错误: ${e.error}';
+        }
+        throw Exception(errorMsg);
+      }
+      
+      throw Exception('上传图片失败: $e');
+    }
+  }
+
+  /// 舌面诊断 2.0 拍照诊断
+  /// [imageUrl] 已上传的图片 URL
+  /// [type] 拍摄类型:'tf' 舌面, 'tb' 舌底, 'ff' 面部
+  /// [appId] 应用 ID
+  /// [appSecret] 应用密钥
+  /// [authCode] 授权码
+  /// [tongueApiBaseUrl] 舌诊 API 基础 URL
+  Future<RecognitionResult> aiRecognizeFaceTongue2(
+    String imageUrl,
+    String type, {
+    required String appId,
+    required String appSecret,
+    required String authCode,
+    required String tongueApiBaseUrl,
+  }) async {
+    try {
+      // 根据类型设置对应的字段名
+      String typeKey = '';
+      switch (type) {
+        case 'ff':
+          typeKey = 'ff_image';
+          break;
+        case 'tf':
+          typeKey = 'tf_image';
+          break;
+        case 'tb':
+          typeKey = 'tb_image';
+          break;
+        default:
+          throw Exception('不支持的拍摄类型: $type');
+      }
+
+      // 构建请求参数
+      final Map<String, dynamic> params = {
+        'scene': 2,
+        'app_id': appId,
+        'app_secret': appSecret,
+        'auth_code': authCode,
+        typeKey: imageUrl,
+      };
+
+      final requestUrl = '$tongueApiBaseUrl/open/api/diagnose/face-tongue/result/v2.0/';
+      developer.log(
+        '开始调用识别接口',
+        name: 'HttpService',
+        error: {
+          'url': requestUrl,
+          'type': type,
+          'typeKey': typeKey,
+          'imageUrl': imageUrl,
+          'appId': appId,
+          'hasAppSecret': appSecret.isNotEmpty,
+          'hasAuthCode': authCode.isNotEmpty,
+        },
+      );
+
+      // 调用识别接口(不使用 baseUrl,使用完整的 tongueApiBaseUrl)
+      final dio = Dio();
+      final response = await dio.post<Map<String, dynamic>>(
+        requestUrl,
+        data: params,
+      );
+
+      developer.log(
+        '识别接口响应',
+        name: 'HttpService',
+        error: {
+          'statusCode': response.statusCode,
+          'hasData': response.data != null,
+          'responseData': response.data,
+        },
+      );
+
+      if (response.data != null) {
+        final data = response.data!;
+        final success = data['success'] == true;
+        
+        if (success && data['data'] != null) {
+          final resultData = data['data'] as Map<String, dynamic>;
+          
+          developer.log(
+            '识别成功,开始解析特征',
+            name: 'HttpService',
+            error: {
+              'hasFeatures': resultData['features'] != null,
+              'featuresCount': resultData['features'] != null 
+                  ? (resultData['features'] as List).length 
+                  : 0,
+            },
+          );
+          
+          // 提取识别特征
+          List<String> results = [];
+          if (resultData['features'] != null) {
+            final features = resultData['features'] as List;
+            for (var feature in features) {
+              if (feature is Map<String, dynamic>) {
+                final featureName = feature['feature_name'] as String?;
+                final featureSituation = feature['feature_situation'] as String?;
+                // 只显示异常特征
+                if (featureName != null && 
+                    featureSituation == '异常' && 
+                    featureName != '正常') {
+                  results.add(featureName);
+                }
+              }
+            }
+          }
+          
+          // 如果没有异常特征,显示"未识别出异常"
+          if (results.isEmpty) {
+            results.add('未识别出异常');
+          }
+
+          developer.log(
+            '识别结果解析完成',
+            name: 'HttpService',
+            error: {
+              'results': results,
+              'resultsCount': results.length,
+            },
+          );
+
+          return RecognitionResult(
+            success: true,
+            results: results,
+          );
+        } else {
+          // 识别失败,返回错误信息
+          final msg = data['msg'] as String? ?? '识别失败';
+          developer.log(
+            '识别失败',
+            name: 'HttpService',
+            error: {
+              'errorMessage': msg,
+              'responseData': data,
+            },
+          );
+          return RecognitionResult(
+            success: false,
+            errorMessage: msg,
+          );
+        }
+      }
+      
+      developer.log(
+        '识别失败:服务器返回数据格式错误',
+        name: 'HttpService',
+        error: {'response': response.data},
+      );
+      
+      return RecognitionResult(
+        success: false,
+        errorMessage: '识别失败:服务器返回数据格式错误',
+      );
+    } catch (e, stackTrace) {
+      developer.log(
+        '识别接口调用异常',
+        name: 'HttpService',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      return RecognitionResult(
+        success: false,
+        errorMessage: '识别失败: $e',
+      );
+    }
+  }
+
+  /// 识别舌面照片
+  Future<RecognitionResult> recognizeTongueSurface(
+    String imagePath, {
+    required String appId,
+    required String appSecret,
+    required String authCode,
+    required String tongueApiBaseUrl,
+    String? businessType,
+  }) async {
+    try {
+      developer.log(
+        '开始识别舌面照片',
+        name: 'HttpService',
+        error: {
+          'imagePath': imagePath,
+          'businessType': businessType,
+          'tongueApiBaseUrl': tongueApiBaseUrl,
+        },
+      );
+
+      // 先上传图片
+      // 注意:上传接口和识别接口在不同的服务器上
+      // 上传接口使用 _baseUrl(已在 HttpService.init 中配置为上传接口的 baseUrl)
+      // 识别接口使用 tongueApiBaseUrl
+      // 注意:上传接口需要 token,但 Flutter 中可能没有 token,先传 null
+      // 如果服务器要求 token,会返回 401,而不是 DNS 错误
+      final imageUrl = await uploadImage(
+        imagePath, 
+        businessType: businessType,
+        token: null, // TODO: 从存储中获取 token(如果需要)
+        // 不传入 uploadBaseUrl,使用 _baseUrl(已配置为上传接口的 baseUrl)
+      );
+      if (imageUrl == null) {
+        developer.log(
+          '识别舌面照片失败:上传图片失败',
+          name: 'HttpService',
+        );
+        return RecognitionResult(
+          success: false,
+          errorMessage: '上传图片失败',
+        );
+      }
+
+      // 调用识别接口
+      final result = await aiRecognizeFaceTongue2(
+        imageUrl,
+        'tf',
+        appId: appId,
+        appSecret: appSecret,
+        authCode: authCode,
+        tongueApiBaseUrl: tongueApiBaseUrl,
+      );
+
+      developer.log(
+        '识别舌面照片完成',
+        name: 'HttpService',
+        error: {
+          'success': result.success,
+          'results': result.results,
+          'errorMessage': result.errorMessage,
+        },
+      );
+
+      return result;
+    } catch (e, stackTrace) {
+      developer.log(
+        '识别舌面照片异常',
+        name: 'HttpService',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      return RecognitionResult(
+        success: false,
+        errorMessage: '识别失败: $e',
+      );
+    }
+  }
+
+  /// 识别舌下脉络
+  Future<RecognitionResult> recognizeSublingualVeins(
+    String imagePath, {
+    required String appId,
+    required String appSecret,
+    required String authCode,
+    required String tongueApiBaseUrl,
+    String? businessType,
+  }) async {
+    try {
+      developer.log(
+        '开始识别舌下脉络',
+        name: 'HttpService',
+        error: {
+          'imagePath': imagePath,
+          'businessType': businessType,
+        },
+      );
+
+      // 先上传图片
+      // 注意:上传接口和识别接口在不同的服务器上
+      // 上传接口使用 _baseUrl(已在 HttpService.init 中配置为上传接口的 baseUrl)
+      // 识别接口使用 tongueApiBaseUrl
+      // 注意:上传接口需要 token,但 Flutter 中可能没有 token,先传 null
+      // 如果服务器要求 token,会返回 401,而不是 DNS 错误
+      final imageUrl = await uploadImage(
+        imagePath, 
+        businessType: businessType,
+        token: null, // TODO: 从存储中获取 token(如果需要)
+        // 不传入 uploadBaseUrl,使用 _baseUrl(已配置为上传接口的 baseUrl)
+      );
+      if (imageUrl == null) {
+        developer.log(
+          '识别舌下脉络失败:上传图片失败',
+          name: 'HttpService',
+        );
+        return RecognitionResult(
+          success: false,
+          errorMessage: '上传图片失败',
+        );
+      }
+
+      // 调用识别接口
+      final result = await aiRecognizeFaceTongue2(
+        imageUrl,
+        'tb',
+        appId: appId,
+        appSecret: appSecret,
+        authCode: authCode,
+        tongueApiBaseUrl: tongueApiBaseUrl,
+      );
+
+      developer.log(
+        '识别舌下脉络完成',
+        name: 'HttpService',
+        error: {
+          'success': result.success,
+          'results': result.results,
+          'errorMessage': result.errorMessage,
+        },
+      );
+
+      return result;
+    } catch (e, stackTrace) {
+      developer.log(
+        '识别舌下脉络异常',
+        name: 'HttpService',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      return RecognitionResult(
+        success: false,
+        errorMessage: '识别失败: $e',
+      );
+    }
+  }
+
+  /// 识别面部照片
+  Future<RecognitionResult> recognizeFace(
+    String imagePath, {
+    required String appId,
+    required String appSecret,
+    required String authCode,
+    required String tongueApiBaseUrl,
+    String? businessType,
+  }) async {
+    try {
+      developer.log(
+        '开始识别面部照片',
+        name: 'HttpService',
+        error: {
+          'imagePath': imagePath,
+          'businessType': businessType,
+        },
+      );
+
+      // 先上传图片
+      // 注意:上传接口和识别接口在不同的服务器上
+      // 上传接口使用 _baseUrl(已在 HttpService.init 中配置为上传接口的 baseUrl)
+      // 识别接口使用 tongueApiBaseUrl
+      // 注意:上传接口需要 token,但 Flutter 中可能没有 token,先传 null
+      // 如果服务器要求 token,会返回 401,而不是 DNS 错误
+      final imageUrl = await uploadImage(
+        imagePath, 
+        businessType: businessType,
+        token: null, // TODO: 从存储中获取 token(如果需要)
+        // 不传入 uploadBaseUrl,使用 _baseUrl(已配置为上传接口的 baseUrl)
+      );
+      if (imageUrl == null) {
+        developer.log(
+          '识别面部照片失败:上传图片失败',
+          name: 'HttpService',
+        );
+        return RecognitionResult(
+          success: false,
+          errorMessage: '上传图片失败',
+        );
+      }
+
+      // 调用识别接口
+      final result = await aiRecognizeFaceTongue2(
+        imageUrl,
+        'ff',
+        appId: appId,
+        appSecret: appSecret,
+        authCode: authCode,
+        tongueApiBaseUrl: tongueApiBaseUrl,
+      );
+
+      developer.log(
+        '识别面部照片完成',
+        name: 'HttpService',
+        error: {
+          'success': result.success,
+          'results': result.results,
+          'errorMessage': result.errorMessage,
+        },
+      );
+
+      return result;
+    } catch (e, stackTrace) {
+      developer.log(
+        '识别面部照片异常',
+        name: 'HttpService',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      return RecognitionResult(
+        success: false,
+        errorMessage: '识别失败: $e',
+      );
+    }
+  }
+
+  /// 上传问诊照片
+  Future<void> uploadDiagnosisImages({
+    String? tongueSurfacePath,
+    String? sublingualVeinsPath,
+    String? facePath,
+  }) async {
+    developer.log(
+      '上传问诊照片',
+      name: 'HttpService',
+      error: {
+        'tongueSurfacePath': tongueSurfacePath,
+        'sublingualVeinsPath': sublingualVeinsPath,
+        'facePath': facePath,
+      },
+    );
+    await Future.delayed(const Duration(seconds: 1));
+  }
+
+  /// 错误处理
+  dynamic _handleError(dynamic error) {
+    if (error is DioException) {
+      developer.log(
+        'HTTP请求错误',
+        name: 'HttpService',
+        error: {
+          'type': error.type.toString(),
+          'message': error.message,
+          'statusCode': error.response?.statusCode,
+          'responseData': error.response?.data,
+        },
+      );
+      switch (error.type) {
+        case DioExceptionType.connectionTimeout:
+        case DioExceptionType.sendTimeout:
+        case DioExceptionType.receiveTimeout:
+          return Exception('请求超时,请检查网络连接');
+        case DioExceptionType.badResponse:
+          return Exception('服务器错误:${error.response?.statusCode}');
+        case DioExceptionType.cancel:
+          return Exception('请求已取消');
+        default:
+          return Exception('网络错误:${error.message}');
+      }
+    }
+    developer.log(
+      '未知错误',
+      name: 'HttpService',
+      error: error,
+    );
+    return error;
+  }
+}
+

+ 2126 - 0
lib/src/widgets/camera_page.dart

@@ -0,0 +1,2126 @@
+import 'dart:developer' as developer;
+import 'dart:io';
+import 'dart:typed_data';
+import 'package:camera/camera.dart';
+import 'package:flutter/material.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:image/image.dart' as img;
+import 'package:my_feature_module/src/models/recognition_result.dart';
+import 'package:my_feature_module/src/services/http_service.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:path/path.dart' as path;
+
+/// 拍照类型
+enum CameraType {
+  tongueSurface, // 舌面
+  sublingualVeins, // 舌下脉络
+  face, // 面部
+}
+
+/// 拍照页面
+class CameraPage extends StatefulWidget {
+  final CameraType type;
+  final Function(String imagePath, Map<String, dynamic>? recognitionResult)? onPhotoTaken;
+  final HttpService? httpService;
+  final String? appId;
+  final String? appSecret;
+  final String? authCode;
+  final String? tongueApiBaseUrl;
+  final String? businessType;
+
+  const CameraPage({
+    super.key,
+    required this.type,
+    this.onPhotoTaken,
+    this.httpService,
+    this.appId,
+    this.appSecret,
+    this.authCode,
+    this.tongueApiBaseUrl,
+    this.businessType,
+  });
+
+  @override
+  State<CameraPage> createState() => _CameraPageState();
+}
+
+class _CameraPageState extends State<CameraPage> with SingleTickerProviderStateMixin {
+  CameraController? _controller;
+  List<CameraDescription>? _cameras;
+  int _currentCameraIndex = 0;
+  bool _isInitialized = false;
+  bool _isCapturing = false;
+  XFile? _capturedImage; // 裁剪后的图片(用于上传和识别)
+  String? _originalImagePath; // 原始完整照片路径(用于识别中背景显示)
+  Map<String, dynamic>? _recognitionResult;
+  final ImagePicker _imagePicker = ImagePicker();
+  AnimationController? _scanAnimationController;
+
+  @override
+  void initState() {
+    super.initState();
+    _initializeCamera();
+    _scanAnimationController = AnimationController(
+      vsync: this,
+      duration: const Duration(seconds: 2),
+    )..repeat();
+  }
+
+  Future<void> _initializeCamera() async {
+    try {
+      _cameras = await availableCameras();
+      if (_cameras == null || _cameras!.isEmpty) {
+        if (mounted) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            const SnackBar(content: Text('未找到相机设备')),
+          );
+          Navigator.of(context).pop();
+        }
+        return;
+      }
+
+      // 默认使用前置摄像头
+      _currentCameraIndex = _cameras!.indexWhere(
+        (camera) => camera.lensDirection == CameraLensDirection.front,
+      );
+      // 如果找不到前置摄像头,使用第一个摄像头
+      if (_currentCameraIndex == -1) {
+        _currentCameraIndex = 0;
+      }
+
+      await _switchCamera(_currentCameraIndex);
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('相机初始化失败: $e')),
+        );
+        Navigator.of(context).pop();
+      }
+    }
+  }
+
+  Future<void> _switchCamera(int cameraIndex) async {
+    if (_cameras == null || cameraIndex < 0 || cameraIndex >= _cameras!.length) {
+      return;
+    }
+
+    // 先释放旧的控制器
+    await _controller?.dispose();
+
+    setState(() {
+      _isInitialized = false;
+    });
+
+    try {
+      _controller = CameraController(
+        _cameras![cameraIndex],
+        ResolutionPreset.veryHigh, // 提高分辨率以满足API要求
+        enableAudio: false,
+      );
+
+      await _controller!.initialize();
+      if (mounted) {
+        setState(() {
+          _currentCameraIndex = cameraIndex;
+          _isInitialized = true;
+        });
+      }
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('切换摄像头失败: $e')),
+        );
+      }
+    }
+  }
+
+  Future<void> _toggleCamera() async {
+    if (_cameras == null || _cameras!.length < 2) {
+      ScaffoldMessenger.of(context).showSnackBar(
+        const SnackBar(content: Text('设备只有一个摄像头')),
+      );
+      return;
+    }
+
+    // 切换到另一个摄像头
+    int nextIndex = (_currentCameraIndex + 1) % _cameras!.length;
+    await _switchCamera(nextIndex);
+  }
+
+  Future<void> _pickImageFromGallery() async {
+    try {
+      developer.log(
+        '从相册选择图片',
+        name: 'CameraPage',
+        error: {'type': widget.type.toString()},
+      );
+
+      final XFile? image = await _imagePicker.pickImage(
+        source: ImageSource.gallery,
+        imageQuality: 85,
+      );
+
+      if (image != null) {
+        developer.log(
+          '图片选择成功',
+          name: 'CameraPage',
+          error: {'imagePath': image.path},
+        );
+
+        // 从相册选择的图片需要检查尺寸是否符合要求
+        final Uint8List imageBytes = await File(image.path).readAsBytes();
+        img.Image? selectedImage = img.decodeImage(imageBytes);
+        
+        if (selectedImage == null) {
+          throw Exception('无法解码图片');
+        }
+        
+        developer.log(
+          '相册图片尺寸检查',
+          name: 'CameraPage',
+          error: {
+            'imageSize': '${selectedImage.width}x${selectedImage.height}',
+          },
+        );
+        
+        // 检查最短边是否满足要求(至少400px,API要求300px,设置为400px以保证质量且避免过度放大)
+        const int minShortEdge = 400;
+        final int shortEdge = selectedImage.width < selectedImage.height 
+            ? selectedImage.width 
+            : selectedImage.height;
+        
+        // 如果最短边小于要求,进行等比例缩放
+        if (shortEdge < minShortEdge) {
+          final double scale = minShortEdge / shortEdge;
+          final int newWidth = (selectedImage.width * scale).round();
+          final int newHeight = (selectedImage.height * scale).round();
+          
+          developer.log(
+            '相册图片尺寸不符合要求,进行缩放',
+            name: 'CameraPage',
+            error: {
+              'originalSize': '${selectedImage.width}x${selectedImage.height}',
+              'scale': scale,
+              'newSize': '${newWidth}x${newHeight}',
+            },
+          );
+          
+          selectedImage = img.copyResize(
+            selectedImage,
+            width: newWidth,
+            height: newHeight,
+            interpolation: img.Interpolation.linear,
+          );
+        }
+        
+        // 保存处理后的图片
+        final Directory tempDir = await getTemporaryDirectory();
+        final String fileName = '${DateTime.now().millisecondsSinceEpoch}.jpg';
+        final String filePath = path.join(tempDir.path, fileName);
+        final File processedFile = File(filePath);
+        await processedFile.writeAsBytes(img.encodeJpg(selectedImage, quality: 90));
+
+        developer.log(
+          '图片保存完成',
+          name: 'CameraPage',
+          error: {
+            'filePath': filePath,
+            'finalSize': '${selectedImage.width}x${selectedImage.height}',
+          },
+        );
+
+        setState(() {
+          _originalImagePath = filePath; // 相册图片原始路径和处理后路径相同
+          _capturedImage = XFile(filePath);
+        });
+
+        developer.log(
+          '开始识别',
+          name: 'CameraPage',
+        );
+        await _simulateRecognition();
+      } else {
+        developer.log(
+          '用户取消选择图片',
+          name: 'CameraPage',
+        );
+      }
+    } catch (e, stackTrace) {
+      developer.log(
+        '选择照片失败',
+        name: 'CameraPage',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('选择照片失败: $e')),
+        );
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller?.dispose();
+    _scanAnimationController?.dispose();
+    super.dispose();
+  }
+
+  Future<void> _takePicture() async {
+    if (!_isInitialized || _controller == null || _isCapturing) return;
+
+    developer.log(
+      '开始拍照',
+      name: 'CameraPage',
+      error: {'type': widget.type.toString()},
+    );
+
+    setState(() {
+      _isCapturing = true;
+    });
+
+    try {
+      final XFile image = await _controller!.takePicture();
+      developer.log(
+        '拍照成功',
+        name: 'CameraPage',
+        error: {'imagePath': image.path},
+      );
+      
+      // 处理原始照片(如果是前置摄像头,需要翻转)
+      String originalPath = image.path;
+      if (_cameras != null && 
+          _currentCameraIndex >= 0 && 
+          _currentCameraIndex < _cameras!.length &&
+          _cameras![_currentCameraIndex].lensDirection == CameraLensDirection.front) {
+        developer.log(
+          '前置摄像头,对原始照片进行镜像翻转',
+          name: 'CameraPage',
+        );
+        
+        // 读取原始图片
+        final Uint8List imageBytes = await File(image.path).readAsBytes();
+        img.Image? originalImage = img.decodeImage(imageBytes);
+        
+        if (originalImage != null) {
+          // 翻转图片
+          originalImage = img.flipHorizontal(originalImage);
+          
+          // 保存翻转后的原始图片
+          final Directory tempDir = await getTemporaryDirectory();
+          final String fileName = '${DateTime.now().millisecondsSinceEpoch}_original_flipped.jpg';
+          final String flippedPath = path.join(tempDir.path, fileName);
+          final File flippedFile = File(flippedPath);
+          await flippedFile.writeAsBytes(img.encodeJpg(originalImage, quality: 90));
+          
+          originalPath = flippedPath;
+          developer.log(
+            '原始照片翻转完成',
+            name: 'CameraPage',
+            error: {'flippedPath': flippedPath},
+          );
+        }
+      }
+      
+      // 裁剪图片,只保留引导框内的部分
+      developer.log(
+        '开始裁剪图片',
+        name: 'CameraPage',
+      );
+      final String croppedImagePath = await _cropImageToGuideBox(image.path);
+      developer.log(
+        '图片裁剪完成',
+        name: 'CameraPage',
+        error: {'croppedImagePath': croppedImagePath},
+      );
+      
+      setState(() {
+        _originalImagePath = originalPath; // 保存原始照片路径(已翻转)
+        _capturedImage = XFile(croppedImagePath); // 保存裁剪后的照片(用于上传)
+      });
+
+      developer.log(
+        '开始识别',
+        name: 'CameraPage',
+      );
+      await _simulateRecognition();
+    } catch (e, stackTrace) {
+      developer.log(
+        '拍照失败',
+        name: 'CameraPage',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('拍照失败: $e')),
+        );
+      }
+    } finally {
+      if (mounted) {
+        setState(() {
+          _isCapturing = false;
+        });
+      }
+    }
+  }
+
+  /// 裁剪图片到引导框区域
+  Future<String> _cropImageToGuideBox(String imagePath) async {
+    try {
+      developer.log(
+        '开始裁剪图片',
+        name: 'CameraPage',
+        error: {'imagePath': imagePath},
+      );
+
+      // 读取原始图片
+      final Uint8List imageBytes = await File(imagePath).readAsBytes();
+      img.Image? originalImage = img.decodeImage(imageBytes);
+      
+      if (originalImage == null) {
+        developer.log(
+          '裁剪失败:无法解码图片',
+          name: 'CameraPage',
+        );
+        throw Exception('无法解码图片');
+      }
+
+      // 获取屏幕尺寸(注意:这里使用屏幕尺寸而不是相机预览尺寸)
+      final Size screenSize = MediaQuery.of(context).size;
+      final Size previewSize = _controller!.value.previewSize ?? Size.zero;
+      final int imageWidth = originalImage.width;
+      final int imageHeight = originalImage.height;
+      
+      // 判断照片是否需要旋转(如果照片是横向的,但屏幕是竖向的,则需要旋转)
+      final bool isImageLandscape = imageWidth > imageHeight;
+      final bool isScreenPortrait = screenSize.width < screenSize.height;
+      final bool needsRotation = isImageLandscape && isScreenPortrait;
+
+      developer.log(
+        '图片尺寸信息',
+        name: 'CameraPage',
+        error: {
+          'screenSize': '${screenSize.width}x${screenSize.height}',
+          'previewSize': '${previewSize.width}x${previewSize.height}',
+          'imageSize': '${imageWidth}x${imageHeight}',
+          'isImageLandscape': isImageLandscape,
+          'isScreenPortrait': isScreenPortrait,
+          'needsRotation': needsRotation,
+        },
+      );
+
+      // 计算引导框在屏幕上的位置和大小(使用屏幕尺寸)
+      final Rect guideRect = _getGuideBoxRect(screenSize);
+
+      // 根据是否需要旋转来计算不同的坐标映射
+      double scaleX, scaleY, scale, offsetX, offsetY;
+      int cropX, cropY, cropWidth, cropHeight;
+      
+      if (needsRotation) {
+        // 照片是横向的,需要旋转90度
+        // 图片的 width 对应屏幕的 height,图片的 height 对应屏幕的 width
+        scaleX = imageHeight / screenSize.width;
+        scaleY = imageWidth / screenSize.height;
+        
+        developer.log(
+          '照片需要旋转90度',
+          name: 'CameraPage',
+          error: {
+            'scaleX': scaleX,
+            'scaleY': scaleY,
+          },
+        );
+        
+        // 使用较大的缩放比例以确保覆盖整个屏幕(BoxFit.cover)
+        scale = scaleX > scaleY ? scaleX : scaleY;
+        
+        // 计算居中偏移
+        offsetX = (imageHeight - screenSize.width * scale) / 2;
+        offsetY = (imageWidth - screenSize.height * scale) / 2;
+        
+        developer.log(
+          '居中偏移',
+          name: 'CameraPage',
+          error: {
+            'scale': scale,
+            'offsetX': offsetX,
+            'offsetY': offsetY,
+          },
+        );
+        
+        // 坐标映射(考虑旋转)
+        cropX = (offsetY + guideRect.top * scale).round();
+        cropY = (offsetX + guideRect.left * scale).round();
+        cropWidth = (guideRect.height * scale).round();
+        cropHeight = (guideRect.width * scale).round();
+      } else {
+        // 照片是竖向的,不需要旋转(或已经是正确方向)
+        scaleX = imageWidth / screenSize.width;
+        scaleY = imageHeight / screenSize.height;
+        
+        developer.log(
+          '照片不需要旋转',
+          name: 'CameraPage',
+          error: {
+            'scaleX': scaleX,
+            'scaleY': scaleY,
+          },
+        );
+        
+        // 使用较大的缩放比例以确保覆盖整个屏幕(BoxFit.cover)
+        scale = scaleX > scaleY ? scaleX : scaleY;
+        
+        // 计算居中偏移
+        offsetX = (imageWidth - screenSize.width * scale) / 2;
+        offsetY = (imageHeight - screenSize.height * scale) / 2;
+        
+        developer.log(
+          '居中偏移',
+          name: 'CameraPage',
+          error: {
+            'scale': scale,
+            'offsetX': offsetX,
+            'offsetY': offsetY,
+          },
+        );
+        
+        // 直接映射坐标
+        cropX = (offsetX + guideRect.left * scale).round();
+        cropY = (offsetY + guideRect.top * scale).round();
+        cropWidth = (guideRect.width * scale).round();
+        cropHeight = (guideRect.height * scale).round();
+      }
+
+      // 确保裁剪区域在图片范围内
+      final int safeX = cropX.clamp(0, imageWidth);
+      final int safeY = cropY.clamp(0, imageHeight);
+      final int safeWidth = (cropX + cropWidth).clamp(0, imageWidth) - safeX;
+      final int safeHeight = (cropY + cropHeight).clamp(0, imageHeight) - safeY;
+
+      developer.log(
+        '裁剪参数',
+        name: 'CameraPage',
+        error: {
+          'guideRect': '${guideRect.left},${guideRect.top},${guideRect.width},${guideRect.height}',
+          'scale': scale,
+          'cropRect': '$safeX,$safeY,$safeWidth,$safeHeight',
+        },
+      );
+
+      // 裁剪图片
+      img.Image croppedImage = img.copyCrop(
+        originalImage,
+        x: safeX,
+        y: safeY,
+        width: safeWidth,
+        height: safeHeight,
+      );
+
+      // 如果照片是横向的但屏幕是竖向的,需要旋转图片
+      if (needsRotation) {
+        developer.log(
+          '旋转图片(逆时针90度)',
+          name: 'CameraPage',
+          error: {
+            'beforeRotation': '${croppedImage.width}x${croppedImage.height}',
+          },
+        );
+        croppedImage = img.copyRotate(croppedImage, angle: -90);
+        
+        developer.log(
+          '旋转后图片尺寸',
+          name: 'CameraPage',
+          error: {
+            'afterRotation': '${croppedImage.width}x${croppedImage.height}',
+          },
+        );
+      } else {
+        developer.log(
+          '照片无需旋转',
+          name: 'CameraPage',
+          error: {
+            'imageSize': '${croppedImage.width}x${croppedImage.height}',
+          },
+        );
+      }
+
+      // 检查裁剪后图片的最短边,确保至少400px(API要求300px,设置为400px以保证质量且避免过度放大)
+      const int minShortEdge = 400;
+      final int shortEdge = croppedImage.width < croppedImage.height 
+          ? croppedImage.width 
+          : croppedImage.height;
+      
+      developer.log(
+        '裁剪后图片尺寸检查',
+        name: 'CameraPage',
+        error: {
+          'croppedSize': '${croppedImage.width}x${croppedImage.height}',
+          'shortEdge': shortEdge,
+          'minRequired': minShortEdge,
+        },
+      );
+      
+      // 如果最短边小于要求的尺寸,进行等比例缩放
+      if (shortEdge < minShortEdge) {
+        final double scale = minShortEdge / shortEdge;
+        final int newWidth = (croppedImage.width * scale).round();
+        final int newHeight = (croppedImage.height * scale).round();
+        
+        developer.log(
+          '图片尺寸不符合要求,进行缩放',
+          name: 'CameraPage',
+          error: {
+            'originalSize': '${croppedImage.width}x${croppedImage.height}',
+            'scale': scale,
+            'newSize': '${newWidth}x${newHeight}',
+          },
+        );
+        
+        croppedImage = img.copyResize(
+          croppedImage,
+          width: newWidth,
+          height: newHeight,
+          interpolation: img.Interpolation.linear,
+        );
+      }
+
+      // 如果是前置摄像头,需要水平翻转图片(镜像翻转)
+      if (_cameras != null && 
+          _currentCameraIndex >= 0 && 
+          _currentCameraIndex < _cameras!.length &&
+          _cameras![_currentCameraIndex].lensDirection == CameraLensDirection.front) {
+        developer.log(
+          '前置摄像头,进行镜像翻转',
+          name: 'CameraPage',
+        );
+        croppedImage = img.flipHorizontal(croppedImage);
+      }
+
+      // 保存裁剪后的图片
+      final Directory tempDir = await getTemporaryDirectory();
+      final String fileName = '${DateTime.now().millisecondsSinceEpoch}_cropped.jpg';
+      final String filePath = path.join(tempDir.path, fileName);
+      final File croppedFile = File(filePath);
+      await croppedFile.writeAsBytes(img.encodeJpg(croppedImage, quality: 90));
+
+      developer.log(
+        '图片裁剪完成',
+        name: 'CameraPage',
+        error: {
+          'originalSize': '${imageWidth}x${imageHeight}',
+          'finalSize': '${croppedImage.width}x${croppedImage.height}',
+          'filePath': filePath,
+        },
+      );
+
+      return filePath;
+    } catch (e, stackTrace) {
+      developer.log(
+        '图片裁剪失败,返回原图',
+        name: 'CameraPage',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      // 如果裁剪失败,返回原图片路径
+      return imagePath;
+    }
+  }
+
+  /// 从相册选择图片后裁剪
+  Future<String> _cropImageFromGallery(String imagePath, Size previewSize) async {
+    try {
+      // 读取原始图片
+      final Uint8List imageBytes = await File(imagePath).readAsBytes();
+      img.Image? originalImage = img.decodeImage(imageBytes);
+      
+      if (originalImage == null) {
+        throw Exception('无法解码图片');
+      }
+
+      // 获取实际图片尺寸
+      final int imageWidth = originalImage.width;
+      final int imageHeight = originalImage.height;
+
+      // 计算引导框在屏幕上的位置和大小
+      final Rect guideRect = _getGuideBoxRect(previewSize);
+
+      // 将屏幕坐标转换为图片坐标
+      // 对于相册图片,我们需要根据图片的实际尺寸和预览尺寸的比例来计算
+      final double scaleX = imageWidth / previewSize.width;
+      final double scaleY = imageHeight / previewSize.height;
+      
+      // 使用较小的缩放比例以确保不超出图片范围
+      final double scale = scaleX < scaleY ? scaleX : scaleY;
+      final int cropX = ((guideRect.left) * scale).round();
+      final int cropY = ((guideRect.top) * scale).round();
+      final int cropWidth = (guideRect.width * scale).round();
+      final int cropHeight = (guideRect.height * scale).round();
+
+      // 确保裁剪区域在图片范围内
+      final int safeX = cropX.clamp(0, imageWidth);
+      final int safeY = cropY.clamp(0, imageHeight);
+      final int safeWidth = (cropX + cropWidth).clamp(0, imageWidth) - safeX;
+      final int safeHeight = (cropY + cropHeight).clamp(0, imageHeight) - safeY;
+
+      // 裁剪图片
+      final img.Image croppedImage = img.copyCrop(
+        originalImage,
+        x: safeX,
+        y: safeY,
+        width: safeWidth,
+        height: safeHeight,
+      );
+
+      // 保存裁剪后的图片
+      final Directory tempDir = await getTemporaryDirectory();
+      final String fileName = '${DateTime.now().millisecondsSinceEpoch}_cropped.jpg';
+      final String filePath = path.join(tempDir.path, fileName);
+      final File croppedFile = File(filePath);
+      await croppedFile.writeAsBytes(img.encodeJpg(croppedImage, quality: 90));
+
+      return filePath;
+    } catch (e) {
+      // 如果裁剪失败,返回原图片路径
+      return imagePath;
+    }
+  }
+
+  /// 获取引导框在屏幕上的矩形区域
+  Rect _getGuideBoxRect(Size screenSize) {
+    final double centerX = screenSize.width / 2;
+    final double centerY = screenSize.height / 2;
+    
+    // 判断是否为横屏/平板(宽高比大于1)
+    final bool isLandscape = screenSize.width > screenSize.height;
+    final double aspectRatio = screenSize.width / screenSize.height;
+
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        // 舌面:嘴巴外轮廓 + 舌头轮廓的边界框
+        // 注意:裁剪时需要给舌头周围留出更多空间,确保AI能识别到完整的舌部区域
+        double width, height;
+        if (isLandscape) {
+          // 横屏时,使用高度作为基准
+          height = screenSize.height * 0.4;
+          width = height * 0.8; // 保持比例
+        } else {
+          width = screenSize.width * 0.6;
+          height = screenSize.height * 0.35;
+        }
+        final double mouthHeight = height * 0.6;
+        final double tongueHeight = height * 0.8;
+        final double tongueBottom = centerY + tongueHeight * 0.5;
+        final double top = centerY - height * 0.1;
+        final double bottom = tongueBottom;
+        
+        // 为舌面拍照增加边距(上下左右各增加30%),确保舌头完整
+        final double originalWidth = width;
+        final double originalHeight = bottom - top;
+        final double paddingX = originalWidth * 0.3;
+        final double paddingY = originalHeight * 0.3;
+        
+        return Rect.fromLTWH(
+          centerX - width / 2 - paddingX,
+          top - paddingY,
+          width + paddingX * 2,
+          bottom - top + paddingY * 2,
+        );
+      case CameraType.sublingualVeins:
+        // 舌下脉络:嘴巴外轮廓的边界框
+        double width, height;
+        if (isLandscape) {
+          height = screenSize.height * 0.3;
+          width = height * 0.7;
+        } else {
+          width = screenSize.width * 0.5;
+          height = screenSize.height * 0.25;
+        }
+        final double mouthHeight = height * 0.7;
+        return Rect.fromCenter(
+          center: Offset(centerX, centerY),
+          width: width,
+          height: mouthHeight,
+        );
+      case CameraType.face:
+        // 面部:人脸轮廓的边界框
+        double width, height;
+        if (isLandscape) {
+          // 横屏时,使用高度作为基准,保持人脸比例
+          height = screenSize.height * 0.6;
+          width = height * 0.75; // 人脸通常比身高稍宽
+        } else {
+          width = screenSize.width * 0.7;
+          height = screenSize.height * 0.5;
+        }
+        return Rect.fromCenter(
+          center: Offset(centerX, centerY),
+          width: width,
+          height: height,
+        );
+    }
+  }
+
+  Future<void> _simulateRecognition() async {
+    final hasConfig = widget.httpService != null &&
+        widget.appId != null &&
+        widget.appSecret != null &&
+        widget.authCode != null &&
+        widget.tongueApiBaseUrl != null;
+
+    developer.log(
+      '开始识别',
+      name: 'CameraPage',
+      error: {
+        'type': widget.type.toString(),
+        'hasConfig': hasConfig,
+        'imagePath': _capturedImage?.path,
+      },
+    );
+
+    if (hasConfig) {
+      try {
+        RecognitionResult result;
+        switch (widget.type) {
+          case CameraType.tongueSurface:
+            developer.log(
+              '调用舌面识别接口',
+              name: 'CameraPage',
+            );
+            result = await widget.httpService!.recognizeTongueSurface(
+              _capturedImage!.path,
+              appId: widget.appId!,
+              appSecret: widget.appSecret!,
+              authCode: widget.authCode!,
+              tongueApiBaseUrl: widget.tongueApiBaseUrl!,
+              businessType: widget.businessType,
+            );
+            break;
+          case CameraType.sublingualVeins:
+            developer.log(
+              '调用舌下脉络识别接口',
+              name: 'CameraPage',
+            );
+            result = await widget.httpService!.recognizeSublingualVeins(
+              _capturedImage!.path,
+              appId: widget.appId!,
+              appSecret: widget.appSecret!,
+              authCode: widget.authCode!,
+              tongueApiBaseUrl: widget.tongueApiBaseUrl!,
+              businessType: widget.businessType,
+            );
+            break;
+          case CameraType.face:
+            developer.log(
+              '调用面部识别接口',
+              name: 'CameraPage',
+            );
+            result = await widget.httpService!.recognizeFace(
+              _capturedImage!.path,
+              appId: widget.appId!,
+              appSecret: widget.appSecret!,
+              authCode: widget.authCode!,
+              tongueApiBaseUrl: widget.tongueApiBaseUrl!,
+              businessType: widget.businessType,
+            );
+            break;
+        }
+
+        developer.log(
+          '识别完成',
+          name: 'CameraPage',
+          error: {
+            'success': result.success,
+            'results': result.results,
+            'errorMessage': result.errorMessage,
+          },
+        );
+
+        _recognitionResult = {
+          'success': result.success,
+          if (result.success)
+            'results': result.results
+          else
+            'errorMessage': result.errorMessage ?? '识别失败',
+        };
+      } catch (e, stackTrace) {
+        developer.log(
+          '识别异常',
+          name: 'CameraPage',
+          error: e,
+          stackTrace: stackTrace,
+        );
+        String errorMessage = '识别失败';
+        switch (widget.type) {
+          case CameraType.tongueSurface:
+            errorMessage = '未检测到图片中舌部区域';
+            break;
+          case CameraType.sublingualVeins:
+            errorMessage = '舌下络脉目标检测失败';
+            break;
+          case CameraType.face:
+            errorMessage = '未检测到图片中面部区域';
+            break;
+        }
+        _recognitionResult = {
+          'success': false,
+          'errorMessage': errorMessage,
+        };
+      }
+    } else {
+      developer.log(
+        '使用模拟识别(未配置接口参数)',
+        name: 'CameraPage',
+      );
+      await Future.delayed(const Duration(seconds: 1));
+      // final random = DateTime.now().millisecond % 3;
+      
+      // if (random == 0) {
+        // switch (widget.type) {
+        //   case CameraType.tongueSurface:
+        //     _recognitionResult = {
+        //       'success': false,
+        //       'errorMessage': '未检测到图片中舌部区域',
+        //     };
+        //     break;
+        //   case CameraType.sublingualVeins:
+        //     _recognitionResult = {
+        //       'success': false,
+        //       'errorMessage': '舌下络脉目标检测失败',
+        //     };
+        //     break;
+        //   case CameraType.face:
+        //     _recognitionResult = {
+        //       'success': false,
+        //       'errorMessage': '未检测到图片中面部区域',
+        //     };
+        //     break;
+        // }
+      // } else {
+        // switch (widget.type) {
+        //   case CameraType.tongueSurface:
+        //     _recognitionResult = {
+        //       'success': true,
+        //       'results': ['舌色红', '舌苔黄'],
+        //     };
+        //     break;
+        //   case CameraType.sublingualVeins:
+        //     _recognitionResult = {
+        //       'success': true,
+        //       'results': ['未识别出异常'],
+        //     };
+        //     break;
+        //   case CameraType.face:
+        //     _recognitionResult = {
+        //       'success': true,
+        //       'results': ['面色黄', '唇紫'],
+        //     };
+        //     break;
+        // }
+      // }
+    }
+    
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  void _confirmPhoto() {
+    if (_capturedImage != null && widget.onPhotoTaken != null) {
+      developer.log(
+        '确认照片',
+        name: 'CameraPage',
+        error: {
+          'imagePath': _capturedImage!.path,
+          'hasRecognitionResult': _recognitionResult != null,
+          'recognitionSuccess': _recognitionResult?['success'],
+        },
+      );
+      widget.onPhotoTaken!(_capturedImage!.path, _recognitionResult);
+      Navigator.of(context).pop();
+    }
+  }
+
+  void _retakePhoto() {
+    setState(() {
+      _capturedImage = null;
+      _originalImagePath = null;
+      _recognitionResult = null;
+    });
+  }
+
+  String _getTitle() {
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        return '拍摄舌面照片';
+      case CameraType.sublingualVeins:
+        return '拍摄舌底照片';
+      case CameraType.face:
+        return '拍摄面部照片';
+    }
+  }
+
+  String _getInstructions() {
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        return '保证光线充足, 不反光\n舌头无异色、异物, 舌面伸展';
+      case CameraType.sublingualVeins:
+        return '保证光线充足、不反光\n嘴巴张开, 舌尖顶住上颚';
+      case CameraType.face:
+        return '保证光线充足、不反光\n正脸、素颜、去掉眼镜等遮挡';
+    }
+  }
+
+  String _getGuideText() {
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        return '将舌头完整放入框内拍摄';
+      case CameraType.sublingualVeins:
+        return '将舌下脉络完整放入框内拍摄';
+      case CameraType.face:
+        return '将脸部完整放入框内拍摄';
+    }
+  }
+
+  /// 计算引导文字的底部位置,确保不遮挡引导框
+  /// 统一方案:优先放在引导框下方,如果空间不足则放在引导框上方
+  double _calculateGuideTextBottomPosition(Size screenSize) {
+    // 获取引导框的位置
+    final Rect guideRect = _getGuideBoxRect(screenSize);
+    
+    // 底部控制按钮区域的高度(包括按钮、padding、间距)
+    const double bottomControlHeight = 110;
+    
+    // 文字高度(包括行高)
+    const double textHeight = 24;
+    
+    // 文字与引导框之间的最小间距(增加间距以避免遮挡)
+    const double minSpacing = 30;
+    
+    // 顶部区域高度(导航栏 + 说明文字)
+    // 导航栏约 60px,说明文字约 50px(包括 padding)
+    const double topAreaHeight = 110;
+    
+    // 引导框底部到屏幕底部的距离
+    final double distanceToBottom = screenSize.height - guideRect.bottom;
+    
+    // 引导框顶部到顶部区域底部的距离
+    final double distanceToTop = guideRect.top - topAreaHeight;
+    
+    // 计算在引导框下方是否有足够空间(需要:间距 + 文字高度 + 底部控制区域)
+    final double requiredSpaceBelow = minSpacing + textHeight + bottomControlHeight;
+    final double spaceBelow = distanceToBottom - requiredSpaceBelow;
+    
+    // 计算在引导框上方是否有足够空间(需要:间距 + 文字高度)
+    final double requiredSpaceAbove = minSpacing + textHeight;
+    final double spaceAbove = distanceToTop - requiredSpaceAbove;
+    
+    if (spaceBelow > 0) {
+      // 空间足够,放在引导框下方
+      // Positioned 的 bottom 是组件底部距离屏幕底部的距离
+      // 文字顶部 = guideRect.bottom + minSpacing
+      // 文字底部 = 文字顶部 + textHeight = guideRect.bottom + minSpacing + textHeight
+      // bottom = screenSize.height - 文字底部
+      final double position = screenSize.height - guideRect.bottom - minSpacing - textHeight;
+      developer.log(
+        '文字放在引导框下方',
+        name: 'CameraPage',
+        error: {
+          'screenHeight': screenSize.height,
+          'guideRectBottom': guideRect.bottom,
+          'position': position,
+          'spaceBelow': spaceBelow,
+          'textTopPosition': guideRect.bottom + minSpacing,
+        },
+      );
+      return position;
+    } else if (spaceAbove > 0) {
+      // 引导框下方空间不足,但上方有空间,放在引导框上方
+      // 文字底部 = guideRect.top - minSpacing
+      // bottom = screenSize.height - 文字底部
+      final double position = screenSize.height - (guideRect.top - minSpacing);
+      developer.log(
+        '文字放在引导框上方',
+        name: 'CameraPage',
+        error: {
+          'screenHeight': screenSize.height,
+          'guideRectTop': guideRect.top,
+          'position': position,
+          'spaceAbove': spaceAbove,
+          'textBottomPosition': guideRect.top - minSpacing,
+        },
+      );
+      return position;
+    } else {
+      // 上下都没有足够空间,放在底部控制按钮上方(最后的选择)
+      final double position = bottomControlHeight + 10;
+      developer.log(
+        '文字放在底部控制按钮上方(空间不足)',
+        name: 'CameraPage',
+        error: {
+          'position': position,
+          'spaceBelow': spaceBelow,
+          'spaceAbove': spaceAbove,
+        },
+      );
+      return position;
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      backgroundColor: const Color(0xFF2C2C2C), // 深灰色背景
+      body: SafeArea(
+        child: _capturedImage == null 
+            ? _buildCameraView() 
+            : (_recognitionResult == null
+                ? _buildRecognizingView() // 识别中状态
+                : (_recognitionResult!['success'] == false
+                    ? _buildFailureView()
+                    : _buildPreviewView())),
+      ),
+    );
+  }
+
+  Widget _buildCameraView() {
+    if (!_isInitialized || _controller == null) {
+      return const Center(
+        child: CircularProgressIndicator(color: Colors.white),
+      );
+    }
+
+    // 获取屏幕尺寸判断是否为横屏
+    final screenSize = MediaQuery.of(context).size;
+    final isLandscape = screenSize.width > screenSize.height;
+
+    return Stack(
+      children: [
+        // 相机预览
+        Positioned.fill(
+          child: CameraPreview(_controller!),
+        ),
+        // 遮罩层和引导框
+        Positioned.fill(
+          child: CustomPaint(
+            painter: CameraOverlayPainter(widget.type),
+          ),
+        ),
+        // 顶部导航栏
+        Positioned(
+          top: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Row(
+              children: [
+                IconButton(
+                  icon: const Icon(Icons.arrow_back, color: Colors.white, size: 24),
+                  onPressed: () => Navigator.of(context).pop(),
+                ),
+                Expanded(
+                  child: Text(
+                    _getTitle(),
+                    style: const TextStyle(
+                      color: Colors.white,
+                      fontSize: 18,
+                      fontWeight: FontWeight.w500,
+                    ),
+                    textAlign: TextAlign.center,
+                  ),
+                ),
+                IconButton(
+                  icon: const Icon(Icons.camera_alt, color: Colors.white, size: 24),
+                  onPressed: () {},
+                ),
+              ],
+            ),
+          ),
+        ),
+        // 说明文字(横屏时移到左上角,竖屏时在顶部居中)
+        if (isLandscape)
+          // 横屏模式:说明文字在左上角
+          Positioned(
+            top: 60,
+            left: 80,
+            width: 180,
+            child: Container(
+              padding: const EdgeInsets.all(10),
+              decoration: BoxDecoration(
+                color: Colors.black.withOpacity(0.6),
+                borderRadius: BorderRadius.circular(8),
+              ),
+              child: Text(
+                _getInstructions(),
+                style: const TextStyle(
+                  color: Colors.white,
+                  fontSize: 11,
+                  height: 1.4,
+                ),
+                textAlign: TextAlign.left,
+              ),
+            ),
+          )
+        else
+          // 竖屏模式:说明文字在顶部
+          Positioned(
+            top: 60,
+            left: 16,
+            right: 16,
+            child: Container(
+              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+              decoration: BoxDecoration(
+                color: Colors.transparent,
+              ),
+              child: Text(
+                _getInstructions(),
+                style: const TextStyle(
+                  color: Colors.white,
+                  fontSize: 14,
+                  height: 1.5,
+                ),
+                textAlign: TextAlign.center,
+              ),
+            ),
+          ),
+        // 底部引导文字(根据拍摄类型和屏幕方向调整位置,避免与引导框重叠)
+        if (!isLandscape)
+          // 竖屏模式:引导文字在底部
+          Positioned(
+            bottom: _calculateGuideTextBottomPosition(screenSize),
+            left: 0,
+            right: 0,
+            child: Center(
+              child: Text(
+                _getGuideText(),
+                style: const TextStyle(
+                  color: Colors.white,
+                  fontSize: 16,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+            ),
+          )
+        else
+          // 横屏模式:引导文字在底部中央
+          Positioned(
+            bottom: 100,
+            left: 0,
+            right: 0,
+            child: Center(
+              child: Container(
+                padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
+                decoration: BoxDecoration(
+                  color: Colors.black.withOpacity(0.6),
+                  borderRadius: BorderRadius.circular(20),
+                ),
+                child: Text(
+                  _getGuideText(),
+                  style: const TextStyle(
+                    color: Colors.white,
+                    fontSize: 14,
+                    fontWeight: FontWeight.w500,
+                  ),
+                  textAlign: TextAlign.center,
+                ),
+              ),
+            ),
+          ),
+        // 左下角缩略图
+        Positioned(
+          bottom: 140,
+          left: 16,
+          child: Container(
+            width: 60,
+            height: 60,
+            decoration: BoxDecoration(
+              color: Colors.grey[800],
+              shape: BoxShape.circle,
+              border: Border.all(color: Colors.white, width: 2),
+            ),
+            child: Icon(
+              widget.type == CameraType.face 
+                  ? Icons.person 
+                  : Icons.face,
+              color: Colors.white70,
+              size: 30,
+            ),
+          ),
+        ),
+        // 底部控制按钮
+        Positioned(
+          bottom: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.symmetric(vertical: 20),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: [
+                IconButton(
+                  icon: const Icon(Icons.flip_camera_ios, color: Colors.white, size: 30),
+                  onPressed: _isInitialized ? _toggleCamera : null,
+                ),
+                GestureDetector(
+                  onTap: _isCapturing ? null : _takePicture,
+                  child: Container(
+                    width: 70,
+                    height: 70,
+                    decoration: BoxDecoration(
+                      shape: BoxShape.circle,
+                      color: Colors.white,
+                      border: Border.all(color: Colors.grey[300]!, width: 4),
+                    ),
+                    child: _isCapturing
+                        ? const Padding(
+                            padding: EdgeInsets.all(20),
+                            child: CircularProgressIndicator(
+                              strokeWidth: 3,
+                              color: Colors.grey,
+                            ),
+                          )
+                        : const SizedBox(),
+                  ),
+                ),
+                IconButton(
+                  icon: const Icon(Icons.photo_library, color: Colors.white, size: 30),
+                  onPressed: _pickImageFromGallery,
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildRecognizingView() {
+    return Stack(
+      children: [
+        // 背景图片(使用原始完整照片)
+        Positioned.fill(
+          child: Image.file(
+            File(_originalImagePath ?? _capturedImage!.path),
+            fit: BoxFit.cover,
+          ),
+        ),
+        // 半透明遮罩
+        Positioned.fill(
+          child: Container(
+            color: Colors.black.withOpacity(0.4),
+          ),
+        ),
+        // 顶部标题栏
+        Positioned(
+          top: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            child: Row(
+              children: [
+                IconButton(
+                  icon: const Icon(Icons.arrow_back, color: Colors.white, size: 24),
+                  onPressed: () => Navigator.of(context).pop(),
+                ),
+                Expanded(
+                  child: Text(
+                    _getTitle(),
+                    style: const TextStyle(
+                      color: Colors.white,
+                      fontSize: 18,
+                      fontWeight: FontWeight.w500,
+                    ),
+                    textAlign: TextAlign.center,
+                  ),
+                ),
+                const SizedBox(width: 48), // 平衡左侧的返回按钮
+              ],
+            ),
+          ),
+        ),
+        // 中心扫描动画
+        Center(
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              // 扫描动画
+              SizedBox(
+                width: 200,
+                height: 200,
+                child: AnimatedBuilder(
+                  animation: _scanAnimationController!,
+                  builder: (context, child) {
+                    return Transform.rotate(
+                      angle: _scanAnimationController!.value * 2 * 3.14159,
+                      child: CustomPaint(
+                        size: const Size(200, 200),
+                        painter: ScanningCirclePainter(_scanAnimationController!.value),
+                      ),
+                    );
+                  },
+                ),
+              ),
+              const SizedBox(height: 32),
+              // 识别中文字
+              const Text(
+                '识别中...',
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 20,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              const SizedBox(height: 8),
+              // 提示文字
+              Text(
+                _getRecognizingText(),
+                style: const TextStyle(
+                  color: Colors.white70,
+                  fontSize: 14,
+                ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  String _getRecognizingText() {
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        return '正在识别舌面特征';
+      case CameraType.sublingualVeins:
+        return '正在识别舌下脉络';
+      case CameraType.face:
+        return '正在识别面部特征';
+    }
+  }
+
+  Widget _buildFailureView() {
+    return Stack(
+      children: [
+        // 背景(深灰色)
+        Positioned.fill(
+          child: Container(
+            color: const Color(0xFF2C2C2C),
+          ),
+        ),
+        // 顶部导航栏
+        Positioned(
+          top: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Row(
+              children: [
+                IconButton(
+                  icon: const Icon(Icons.arrow_back, color: Colors.white, size: 24),
+                  onPressed: () => Navigator.of(context).pop(),
+                ),
+                Expanded(
+                  child: Text(
+                    _getTitle(),
+                    style: const TextStyle(
+                      color: Colors.white,
+                      fontSize: 18,
+                      fontWeight: FontWeight.w500,
+                    ),
+                    textAlign: TextAlign.center,
+                  ),
+                ),
+                IconButton(
+                  icon: const Icon(Icons.camera_alt, color: Colors.white, size: 24),
+                  onPressed: () {},
+                ),
+              ],
+            ),
+          ),
+        ),
+        // 说明文字
+        Positioned(
+          top: 60,
+          left: 16,
+          right: 16,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Text(
+              _getInstructions(),
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 14,
+                height: 1.5,
+              ),
+              textAlign: TextAlign.center,
+            ),
+          ),
+        ),
+        // 错误信息卡片
+        Positioned(
+          top: 120,
+          left: 16,
+          right: 16,
+          child: Container(
+            padding: const EdgeInsets.all(16),
+            decoration: BoxDecoration(
+              color: Colors.white,
+              borderRadius: BorderRadius.circular(12),
+            ),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    const Text(
+                      '识别失败',
+                      style: TextStyle(
+                        fontSize: 16,
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                    GestureDetector(
+                      onTap: _retakePhoto,
+                      child: const Icon(Icons.close, size: 20, color: Colors.grey),
+                    ),
+                  ],
+                ),
+                const SizedBox(height: 16),
+                // 相机预览区域(显示拍摄的图片,带蓝色边框)
+                Container(
+                  width: double.infinity,
+                  constraints: const BoxConstraints(maxHeight: 250),
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(8),
+                    border: Border.all(
+                      color: const Color(0xFF4FC3F7),
+                      width: 2,
+                    ),
+                    boxShadow: [
+                      BoxShadow(
+                        color: const Color(0xFF4FC3F7).withOpacity(0.3),
+                        blurRadius: 8,
+                        spreadRadius: 2,
+                      ),
+                    ],
+                  ),
+                  child: Stack(
+                    children: [
+                      ClipRRect(
+                        borderRadius: BorderRadius.circular(6),
+                        child: Image.file(
+                          File(_capturedImage!.path),
+                          fit: BoxFit.cover,
+                          width: double.infinity,
+                          height: double.infinity,
+                        ),
+                      ),
+                      // 错误信息横幅(在图片底部)
+                      Positioned(
+                        bottom: 0,
+                        left: 0,
+                        right: 0,
+                        child: Container(
+                          padding: const EdgeInsets.symmetric(
+                            horizontal: 12,
+                            vertical: 8,
+                          ),
+                          decoration: BoxDecoration(
+                            color: Colors.grey[900]!.withOpacity(0.8),
+                            borderRadius: const BorderRadius.only(
+                              bottomLeft: Radius.circular(6),
+                              bottomRight: Radius.circular(6),
+                            ),
+                          ),
+                          child: Text(
+                            _recognitionResult!['errorMessage'] ?? '识别失败',
+                            style: const TextStyle(
+                              fontSize: 14,
+                              color: Colors.white,
+                            ),
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+        // 引导文字和示例图片
+        Positioned(
+          bottom: 100,
+          left: 16,
+          right: 16,
+          child: Column(
+            children: [
+              const Text(
+                '请参照下图正确的方式重新拍摄',
+                style: TextStyle(
+                  color: Colors.white,
+                  fontSize: 14,
+                ),
+              ),
+              const SizedBox(height: 16),
+              // 示例图片对比
+              Row(
+                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                children: _buildExampleImages(),
+              ),
+            ],
+          ),
+        ),
+        // 底部重新拍摄按钮
+        Positioned(
+          bottom: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.all(16),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: SizedBox(
+              width: double.infinity,
+              height: 50,
+              child: ElevatedButton(
+                onPressed: _retakePhoto,
+                style: ElevatedButton.styleFrom(
+                  backgroundColor: const Color(0xFF4FC3F7),
+                  foregroundColor: Colors.white,
+                  shape: RoundedRectangleBorder(
+                    borderRadius: BorderRadius.circular(25),
+                  ),
+                ),
+                child: const Text(
+                  '重新拍摄',
+                  style: TextStyle(
+                    fontSize: 16,
+                    fontWeight: FontWeight.bold,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  List<Widget> _buildExampleImages() {
+    List<Map<String, dynamic>> examples = [];
+    
+    switch (widget.type) {
+      case CameraType.tongueSurface:
+        examples = [
+          {'label': '正确', 'icon': Icons.check_circle, 'color': Colors.green},
+          {'label': '太偏', 'icon': Icons.swap_horiz, 'color': Colors.orange},
+          {'label': '太小', 'icon': Icons.zoom_out, 'color': Colors.orange},
+          {'label': '看不全', 'icon': Icons.visibility_off, 'color': Colors.red},
+        ];
+        break;
+      case CameraType.sublingualVeins:
+        examples = [
+          {'label': '正确', 'icon': Icons.check_circle, 'color': Colors.green},
+          {'label': '太偏', 'icon': Icons.swap_horiz, 'color': Colors.orange},
+          {'label': '太小', 'icon': Icons.zoom_out, 'color': Colors.orange},
+          {'label': '看不全', 'icon': Icons.visibility_off, 'color': Colors.red},
+        ];
+        break;
+      case CameraType.face:
+        examples = [
+          {'label': '正确', 'icon': Icons.check_circle, 'color': Colors.green},
+          {'label': '太偏', 'icon': Icons.swap_horiz, 'color': Colors.orange},
+          {'label': '太小', 'icon': Icons.zoom_out, 'color': Colors.orange},
+          {'label': '非正面', 'icon': Icons.swap_horiz, 'color': Colors.red},
+        ];
+        break;
+    }
+
+    return examples.map((example) {
+      return Column(
+        children: [
+          Container(
+            width: 60,
+            height: 60,
+            decoration: BoxDecoration(
+              color: Colors.white,
+              shape: BoxShape.circle,
+              border: Border.all(
+                color: example['color'] as Color,
+                width: 2,
+              ),
+            ),
+            child: Icon(
+              example['icon'] as IconData,
+              color: example['color'] as Color,
+              size: 30,
+            ),
+          ),
+          const SizedBox(height: 4),
+          Text(
+            example['label'] as String,
+            style: const TextStyle(
+              color: Colors.white,
+              fontSize: 12,
+            ),
+          ),
+        ],
+      );
+    }).toList();
+  }
+
+  Widget _buildPreviewView() {
+    return Stack(
+      children: [
+        // 背景(深灰色)
+        Positioned.fill(
+          child: Container(
+            color: const Color(0xFF2C2C2C),
+          ),
+        ),
+        // 顶部导航栏
+        Positioned(
+          top: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Row(
+              children: [
+                IconButton(
+                  icon: const Icon(Icons.arrow_back, color: Colors.white, size: 24),
+                  onPressed: () => Navigator.of(context).pop(),
+                ),
+                Expanded(
+                  child: Text(
+                    _getTitle(),
+                    style: const TextStyle(
+                      color: Colors.white,
+                      fontSize: 18,
+                      fontWeight: FontWeight.w500,
+                    ),
+                    textAlign: TextAlign.center,
+                  ),
+                ),
+                IconButton(
+                  icon: const Icon(Icons.camera_alt, color: Colors.white, size: 24),
+                  onPressed: () {},
+                ),
+              ],
+            ),
+          ),
+        ),
+        // 说明文字
+        Positioned(
+          top: 60,
+          left: 16,
+          right: 16,
+          child: Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Text(
+              _getInstructions(),
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 14,
+                height: 1.5,
+              ),
+              textAlign: TextAlign.center,
+            ),
+          ),
+        ),
+        // 识别结果卡片(带关闭按钮)
+        if (_recognitionResult != null && _recognitionResult!['success'] == true)
+          Positioned(
+            top: 120,
+            left: 16,
+            right: 16,
+            child: Container(
+              padding: const EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                color: Colors.white,
+                borderRadius: BorderRadius.circular(12),
+              ),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      const Text(
+                        '识别结果',
+                        style: TextStyle(
+                          fontSize: 16,
+                          fontWeight: FontWeight.bold,
+                        ),
+                      ),
+                      GestureDetector(
+                        onTap: _retakePhoto,
+                        child: const Icon(Icons.close, size: 20, color: Colors.grey),
+                      ),
+                    ],
+                  ),
+                  const SizedBox(height: 16),
+                  // 拍摄的图片(带蓝色发光边框)
+                  Container(
+                    width: double.infinity,
+                    constraints: const BoxConstraints(maxHeight: 300),
+                    decoration: BoxDecoration(
+                      borderRadius: BorderRadius.circular(8),
+                      border: Border.all(
+                        color: const Color(0xFF4FC3F7),
+                        width: 2,
+                      ),
+                      boxShadow: [
+                        BoxShadow(
+                          color: const Color(0xFF4FC3F7).withOpacity(0.3),
+                          blurRadius: 8,
+                          spreadRadius: 2,
+                        ),
+                      ],
+                    ),
+                    child: ClipRRect(
+                      borderRadius: BorderRadius.circular(6),
+                      child: Image.file(
+                        File(_capturedImage!.path),
+                        fit: BoxFit.contain,
+                      ),
+                    ),
+                  ),
+                  const SizedBox(height: 16),
+                  // 识别结果标签
+                  Wrap(
+                    spacing: 8,
+                    runSpacing: 8,
+                    children: (_recognitionResult!['results'] as List)
+                        .map((result) => Container(
+                              padding: const EdgeInsets.symmetric(
+                                horizontal: 12,
+                                vertical: 6,
+                              ),
+                              decoration: BoxDecoration(
+                                color: Colors.grey[200],
+                                borderRadius: BorderRadius.circular(16),
+                              ),
+                              child: Text(
+                                result.toString(),
+                                style: TextStyle(
+                                  fontSize: 14,
+                                  color: Colors.grey[800],
+                                ),
+                              ),
+                            ))
+                        .toList(),
+                  ),
+                ],
+              ),
+            ),
+          ),
+        // 底部按钮
+        Positioned(
+          bottom: 0,
+          left: 0,
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.all(16),
+            decoration: BoxDecoration(
+              color: Colors.transparent,
+            ),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: [
+                Expanded(
+                  child: OutlinedButton(
+                    onPressed: _retakePhoto,
+                    style: OutlinedButton.styleFrom(
+                      foregroundColor: Colors.white,
+                      side: const BorderSide(color: Colors.white),
+                      padding: const EdgeInsets.symmetric(vertical: 12),
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(25),
+                      ),
+                    ),
+                    child: const Text('重新拍摄'),
+                  ),
+                ),
+                const SizedBox(width: 16),
+                Expanded(
+                  child: ElevatedButton(
+                    onPressed: _confirmPhoto,
+                    style: ElevatedButton.styleFrom(
+                      backgroundColor: const Color(0xFF4FC3F7),
+                      foregroundColor: Colors.white,
+                      padding: const EdgeInsets.symmetric(vertical: 12),
+                      shape: RoundedRectangleBorder(
+                        borderRadius: BorderRadius.circular(25),
+                      ),
+                    ),
+                    child: const Text('完成'),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+/// 相机遮罩绘制器
+class CameraOverlayPainter extends CustomPainter {
+  final CameraType type;
+
+  CameraOverlayPainter(this.type);
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final centerX = size.width / 2;
+    final centerY = size.height / 2;
+
+    // 创建遮罩路径(整个屏幕)
+    final maskPath = Path()
+      ..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
+
+    // 根据类型创建引导框路径(需要排除的区域)
+    Path guidePath;
+    switch (type) {
+      case CameraType.tongueSurface:
+        guidePath = _getTongueSurfacePath(centerX, centerY, size);
+        break;
+      case CameraType.sublingualVeins:
+        guidePath = _getSublingualVeinsPath(centerX, centerY, size);
+        break;
+      case CameraType.face:
+        guidePath = _getFacePath(centerX, centerY, size);
+        break;
+    }
+
+    // 从遮罩路径中排除引导框路径(使用 PathOperation.difference)
+    final finalPath = Path.combine(
+      PathOperation.difference,
+      maskPath,
+      guidePath,
+    );
+
+    // 绘制半透明遮罩(排除引导框区域)
+    final maskPaint = Paint()
+      ..color = Colors.black.withValues(alpha: 0.6)
+      ..style = PaintingStyle.fill;
+    canvas.drawPath(finalPath, maskPaint);
+
+    // 绘制引导框边框
+    final guidePaint = Paint()
+      ..color = Colors.white
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = 3;
+    canvas.drawPath(guidePath, guidePaint);
+
+    // 对于舌下脉络,需要绘制中间的垂直线(舌系带)
+    if (type == CameraType.sublingualVeins) {
+      final bool isLandscape = size.width > size.height;
+      final double width = isLandscape
+          ? size.height * 0.3 * 0.7
+          : size.width * 0.5;
+      final double height = isLandscape
+          ? size.height * 0.3
+          : size.height * 0.25;
+      final double veinHeight = height * 0.6;
+      final double veinTop = centerY - height * 0.15;
+      final double veinBottom = centerY + veinHeight * 0.3;
+      
+      canvas.drawLine(
+        Offset(centerX, veinTop),
+        Offset(centerX, veinBottom),
+        guidePaint,
+      );
+    }
+  }
+
+  // 获取舌面引导框路径
+  Path _getTongueSurfacePath(double centerX, double centerY, Size size) {
+    final bool isLandscape = size.width > size.height;
+    final double width = isLandscape 
+        ? size.height * 0.4 * 0.8  // 横屏时使用高度作为基准
+        : size.width * 0.6;
+    final double height = isLandscape
+        ? size.height * 0.4
+        : size.height * 0.35;
+    
+    // 创建嘴巴外轮廓路径(椭圆形)
+    final mouthPath = Path()
+      ..addOval(Rect.fromCenter(
+        center: Offset(centerX, centerY),
+        width: width,
+        height: height * 0.6,
+      ));
+    
+    // 创建舌头轮廓路径(大U形)
+    final tonguePath = Path();
+    final tongueWidth = width * 0.7;
+    final tongueHeight = height * 0.8;
+    final tongueTop = centerY - height * 0.1;
+    final tongueLeft = centerX - tongueWidth / 2;
+    final tongueRight = centerX + tongueWidth / 2;
+    final tongueBottom = centerY + tongueHeight * 0.5;
+    
+    tonguePath.moveTo(tongueLeft, tongueTop);
+    tonguePath.quadraticBezierTo(
+      tongueLeft,
+      tongueBottom,
+      centerX,
+      tongueBottom,
+    );
+    tonguePath.quadraticBezierTo(
+      tongueRight,
+      tongueBottom,
+      tongueRight,
+      tongueTop,
+    );
+    tonguePath.close();
+    
+    // 合并嘴巴和舌头路径
+    return Path.combine(PathOperation.union, mouthPath, tonguePath);
+  }
+
+  // 获取舌下脉络引导框路径
+  Path _getSublingualVeinsPath(double centerX, double centerY, Size size) {
+    final bool isLandscape = size.width > size.height;
+    final double width = isLandscape
+        ? size.height * 0.3 * 0.7  // 横屏时使用高度作为基准
+        : size.width * 0.5;
+    final double height = isLandscape
+        ? size.height * 0.3
+        : size.height * 0.25;
+    
+    // 创建嘴巴外轮廓路径
+    final mouthPath = Path()
+      ..addOval(Rect.fromCenter(
+        center: Offset(centerX, centerY),
+        width: width,
+        height: height * 0.7,
+      ));
+    
+    // 创建舌下脉络轮廓路径(小弧形)
+    final veinPath = Path();
+    final veinWidth = width * 0.5;
+    final veinHeight = height * 0.6;
+    final veinTop = centerY - height * 0.15;
+    final veinLeft = centerX - veinWidth / 2;
+    final veinRight = centerX + veinWidth / 2;
+    final veinBottom = centerY + veinHeight * 0.3;
+    
+    veinPath.moveTo(veinLeft, veinTop);
+    veinPath.quadraticBezierTo(
+      centerX,
+      veinBottom,
+      veinRight,
+      veinTop,
+    );
+    veinPath.close();
+    
+    // 合并嘴巴和舌下路径
+    return Path.combine(PathOperation.union, mouthPath, veinPath);
+  }
+
+  // 获取面部引导框路径
+  Path _getFacePath(double centerX, double centerY, Size size) {
+    final bool isLandscape = size.width > size.height;
+    final double width = isLandscape
+        ? size.height * 0.6 * 0.75  // 横屏时使用高度作为基准,保持人脸比例
+        : size.width * 0.7;
+    final double height = isLandscape
+        ? size.height * 0.6
+        : size.height * 0.5;
+    
+    // 创建人脸轮廓路径(椭圆形)
+    return Path()
+      ..addOval(Rect.fromCenter(
+        center: Offset(centerX, centerY),
+        width: width,
+        height: height,
+      ));
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
+
+/// 扫描圆环绘制器
+class ScanningCirclePainter extends CustomPainter {
+  final double progress;
+
+  ScanningCirclePainter(this.progress);
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final center = Offset(size.width / 2, size.height / 2);
+    final radius = size.width / 2;
+
+    // 绘制外圈(半透明)
+    final outerPaint = Paint()
+      ..color = const Color(0xFF4FC3F7).withOpacity(0.2)
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = 2;
+    canvas.drawCircle(center, radius, outerPaint);
+
+    // 绘制扫描弧线(渐变效果)
+    final scanPaint = Paint()
+      ..color = const Color(0xFF4FC3F7)
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = 4
+      ..strokeCap = StrokeCap.round;
+
+    // 绘制多个弧线形成扫描效果
+    for (int i = 0; i < 3; i++) {
+      final angle = (progress * 2 * 3.14159) - (i * 0.3);
+      final opacity = 1.0 - (i * 0.3);
+      scanPaint.color = Color(0xFF4FC3F7).withOpacity(opacity);
+      
+      canvas.drawArc(
+        Rect.fromCircle(center: center, radius: radius - (i * 10)),
+        angle,
+        0.8,
+        false,
+        scanPaint,
+      );
+    }
+
+    // 绘制内圈装饰线
+    final innerPaint = Paint()
+      ..color = const Color(0xFF4FC3F7).withOpacity(0.3)
+      ..style = PaintingStyle.stroke
+      ..strokeWidth = 1;
+    canvas.drawCircle(center, radius * 0.6, innerPaint);
+  }
+
+  @override
+  bool shouldRepaint(ScanningCirclePainter oldDelegate) => 
+      oldDelegate.progress != progress;
+}
+

+ 595 - 0
lib/src/widgets/smz_page.dart

@@ -0,0 +1,595 @@
+import 'dart:developer' as developer;
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:my_feature_module/src/models/recognition_result.dart';
+import 'package:my_feature_module/src/services/http_service.dart';
+import 'package:my_feature_module/src/widgets/camera_page.dart';
+
+class SmzPage extends StatefulWidget {
+  const SmzPage({super.key});
+
+  @override
+  State<SmzPage> createState() => _SmzPageState();
+}
+
+class _SmzPageState extends State<SmzPage> {
+  final ScrollController _controller = ScrollController();
+  final HttpService _httpService = HttpService();
+
+  // 三个图片数据
+  ImageData _tongueSurface = ImageData();
+  ImageData _sublingualVeins = ImageData();
+  ImageData _face = ImageData();
+
+  // 舌面诊接口配置
+  static const String _appId = '58928655-2a2b-4177-a81e-88ce7e272485';
+  static const String _appSecret = '7a3d4f1a-a8ad-494e-9f72-bbcec7ae230f';
+  static const String _authCode = '4e06c40b3b61432d9889b041ea27dab9';
+  // 识别接口的 baseUrl
+  // static const String _tongueApiBaseUrl = 'https://api.macrocura.com';//生产
+  static const String _tongueApiBaseUrl = 'https://qaapi.macrocura.com';//测试
+  // 上传接口的 baseUrl
+  // static const String _uploadApiBaseUrl = 'https://api.lightcura.com';//生产
+  static const String _uploadApiBaseUrl = 'https://qaapi.lightcura.com';//测试
+  static const String _businessType = 'mini_tongue';
+
+  @override
+  void initState() {
+    super.initState();
+    // 初始化 HTTP 服务
+    // 注意:上传接口可能在不同的服务器上,需要单独配置 baseUrl
+    // 如果上传接口和识别接口在同一服务器,则使用相同的 baseUrl
+    _httpService.init(
+      baseUrl: _uploadApiBaseUrl, // 使用上传接口的 baseUrl
+    );
+    
+    developer.log(
+      'SmzPage 初始化完成',
+      name: 'SmzPage',
+      error: {
+        'hasHttpService': true,
+        'appId': _appId,
+        'tongueApiBaseUrl': _tongueApiBaseUrl,
+        'uploadApiBaseUrl': _uploadApiBaseUrl,
+        'businessType': _businessType,
+      },
+    );
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  /// 检查是否可以提交(至少有一张图片)
+  bool get _canSubmit {
+    return _tongueSurface.hasImage ||
+        _sublingualVeins.hasImage ||
+        _face.hasImage;
+  }
+
+  /// 跳转到拍照页面
+  Future<void> _navigateToCamera(CameraType type) async {
+    await Navigator.of(context).push(
+      MaterialPageRoute(
+        builder: (context) => CameraPage(
+          type: type,
+          httpService: _httpService,
+          appId: _appId,
+          appSecret: _appSecret,
+          authCode: _authCode,
+          tongueApiBaseUrl: _tongueApiBaseUrl,
+          businessType: _businessType,
+          onPhotoTaken: (imagePath, recognitionResult) {
+            RecognitionResult? recognition;
+            if (recognitionResult != null) {
+              recognition = RecognitionResult(
+                success: recognitionResult['success'] ?? false,
+                results: recognitionResult['results'] != null
+                    ? List<String>.from(recognitionResult['results'])
+                    : [],
+              );
+            }
+
+            if (mounted) {
+              setState(() {
+                switch (type) {
+                  case CameraType.tongueSurface:
+                    _tongueSurface = ImageData(
+                      imagePath: imagePath,
+                      recognitionResult: recognition,
+                      showExample: false,
+                    );
+                    break;
+                  case CameraType.sublingualVeins:
+                    _sublingualVeins = ImageData(
+                      imagePath: imagePath,
+                      recognitionResult: recognition,
+                      showExample: false,
+                    );
+                    break;
+                  case CameraType.face:
+                    _face = ImageData(
+                      imagePath: imagePath,
+                      recognitionResult: recognition,
+                      showExample: false,
+                    );
+                    break;
+                }
+              });
+            }
+          },
+        ),
+      ),
+    );
+  }
+
+  /// 删除图片
+  void _deleteImage(String type) {
+    setState(() {
+      switch (type) {
+        case 'tongueSurface':
+          _tongueSurface = ImageData(showExample: true);
+          break;
+        case 'sublingualVeins':
+          _sublingualVeins = ImageData(showExample: true);
+          break;
+        case 'face':
+          _face = ImageData(showExample: true);
+          break;
+      }
+    });
+  }
+
+  /// 提交
+  Future<void> _submit() async {
+    if (!_canSubmit) {
+      developer.log(
+        '提交失败:至少需要一张图片',
+        name: 'SmzPage',
+      );
+      ScaffoldMessenger.of(context).showSnackBar(
+        const SnackBar(content: Text('请至少上传一张图片')),
+      );
+      return;
+    }
+
+    developer.log(
+      '开始提交问诊照片',
+      name: 'SmzPage',
+      error: {
+        'tongueSurfacePath': _tongueSurface.imagePath,
+        'sublingualVeinsPath': _sublingualVeins.imagePath,
+        'facePath': _face.imagePath,
+      },
+    );
+
+    try {
+      await _httpService.uploadDiagnosisImages(
+        tongueSurfacePath: _tongueSurface.imagePath,
+        sublingualVeinsPath: _sublingualVeins.imagePath,
+        facePath: _face.imagePath,
+      );
+
+      developer.log(
+        '提交成功',
+        name: 'SmzPage',
+      );
+
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          const SnackBar(content: Text('上传成功,医生将进一步分析')),
+        );
+        Navigator.of(context).pop();
+      }
+    } catch (e, stackTrace) {
+      developer.log(
+        '提交失败',
+        name: 'SmzPage',
+        error: e,
+        stackTrace: stackTrace,
+      );
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('上传失败: $e')),
+        );
+      }
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final bool isLandscape = MediaQuery.of(context).size.width > MediaQuery.of(context).size.height;
+    final double maxWidth = isLandscape ? 800 : double.infinity;
+    
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('舌面诊'),
+        backgroundColor: Colors.white,
+        elevation: 0,
+      ),
+      body: Center(
+        child: ConstrainedBox(
+          constraints: BoxConstraints(maxWidth: maxWidth),
+          child: Column(
+            children: [
+              // 头部区域
+              // _buildHeader(),
+              // 主要内容区域
+              Expanded(
+                child: SingleChildScrollView(
+                  controller: _controller,
+                  padding: const EdgeInsets.all(16),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      // 舌部部分
+                      _buildSectionTitle('舌部'),
+                      const SizedBox(height: 16),
+                      _buildImageUploadRow(
+                        '舌面',
+                        _tongueSurface,
+                        () => _navigateToCamera(CameraType.tongueSurface),
+                        () => _deleteImage('tongueSurface'),
+                      ),
+                      const SizedBox(height: 16),
+                      _buildImageUploadRow(
+                        '舌下脉络',
+                        _sublingualVeins,
+                        () => _navigateToCamera(CameraType.sublingualVeins),
+                        () => _deleteImage('sublingualVeins'),
+                      ),
+                      const SizedBox(height: 32),
+                      // 面部部分
+                      _buildSectionTitle('面部'),
+                      const SizedBox(height: 16),
+                      _buildImageUploadRow(
+                        '正面',
+                        _face,
+                        () => _navigateToCamera(CameraType.face),
+                        () => _deleteImage('face'),
+                      ),
+                      const SizedBox(height: 100), // 为底部按钮留出空间
+                    ],
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+      bottomNavigationBar: _buildBottomButton(),
+    );
+  }
+
+  // 头部区域
+  Widget _buildHeader() {
+    return Container(
+      padding: const EdgeInsets.all(20),
+      decoration: BoxDecoration(
+        color: const Color(0xFFE8F5E9), // 浅绿色背景
+        borderRadius: const BorderRadius.only(
+          bottomLeft: Radius.circular(20),
+          bottomRight: Radius.circular(20),
+        ),
+      ),
+      child: Row(
+        children: [
+          // 左侧房子图标
+          IconButton(
+            icon: const Icon(Icons.home, color: Colors.grey),
+            onPressed: () {
+              Navigator.of(context).pop();
+            },
+          ),
+          const SizedBox(width: 8),
+          // 中间标题和副标题
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                const Text(
+                  '智能舌面诊',
+                  style: TextStyle(
+                    fontSize: 20,
+                    fontWeight: FontWeight.bold,
+                    color: Colors.black87,
+                  ),
+                ),
+                const SizedBox(height: 4),
+                Text(
+                  '极速识别舌面异常',
+                  style: TextStyle(
+                    fontSize: 14,
+                    color: Colors.grey[600],
+                  ),
+                ),
+              ],
+            ),
+          ),
+          // 右侧图标(舌头扫描效果)
+          Container(
+            width: 60,
+            height: 60,
+            decoration: BoxDecoration(
+              color: Colors.green[100],
+              shape: BoxShape.circle,
+            ),
+            child: Icon(
+              Icons.face,
+              size: 30,
+              color: Colors.green[700],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 部分标题
+  Widget _buildSectionTitle(String title) {
+    return Text(
+      title,
+      style: const TextStyle(
+        fontSize: 18,
+        fontWeight: FontWeight.bold,
+        color: Colors.black87,
+      ),
+    );
+  }
+
+  // 图片上传行
+  Widget _buildImageUploadRow(
+    String label,
+    ImageData imageData,
+    VoidCallback onTap,
+    VoidCallback onDelete,
+  ) {
+    return Row(
+      children: [
+        // 左侧上传框
+        Expanded(
+          flex: 2,
+          child: GestureDetector(
+            onTap: imageData.hasImage && !imageData.showExample ? null : onTap,
+            child: Container(
+              height: 120,
+              decoration: BoxDecoration(
+                border: Border.all(
+                  color: Colors.grey[300]!,
+                  style: BorderStyle.solid,
+                  width: 1,
+                ),
+                borderRadius: BorderRadius.circular(8),
+              ),
+              child: imageData.hasImage && !imageData.showExample
+                  ? Stack(
+                      children: [
+                        ClipRRect(
+                          borderRadius: BorderRadius.circular(8),
+                          child: Image.file(
+                            File(imageData.imagePath!),
+                            width: double.infinity,
+                            height: double.infinity,
+                            fit: BoxFit.cover,
+                          ),
+                        ),
+                        // 删除按钮
+                        Positioned(
+                          top: 4,
+                          right: 4,
+                          child: GestureDetector(
+                            onTap: onDelete,
+                            child: Container(
+                              padding: const EdgeInsets.all(4),
+                              decoration: const BoxDecoration(
+                                color: Colors.black54,
+                                shape: BoxShape.circle,
+                              ),
+                              child: const Icon(
+                                Icons.close,
+                                color: Colors.white,
+                                size: 16,
+                              ),
+                            ),
+                          ),
+                        ),
+                      ],
+                    )
+                  : Column(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Icon(
+                          Icons.add_circle_outline,
+                          size: 40,
+                          color: Colors.grey[400],
+                        ),
+                        const SizedBox(height: 8),
+                        Text(
+                          label,
+                          style: TextStyle(
+                            fontSize: 14,
+                            color: Colors.grey[600],
+                          ),
+                        ),
+                      ],
+                    ),
+            ),
+          ),
+        ),
+        const SizedBox(width: 16),
+        // 右侧:识别结果或示例图片
+        Expanded(
+          flex: 2,
+          child: imageData.hasImage && !imageData.showExample
+              ? _buildRecognitionResultOrExample(imageData, label)
+              : Container(
+                  height: 120,
+                  decoration: BoxDecoration(
+                    color: Colors.grey[100],
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Icon(
+                        label == '舌面' || label == '舌下脉络'
+                            ? Icons.face
+                            : Icons.person,
+                        size: 50,
+                        color: Colors.grey[400],
+                      ),
+                      const SizedBox(height: 8),
+                      const Text(
+                        '示例',
+                        style: TextStyle(
+                          fontSize: 12,
+                          color: Colors.grey,
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+        ),
+      ],
+    );
+  }
+
+  // 构建识别结果或示例图
+  Widget _buildRecognitionResultOrExample(ImageData imageData, String label) {
+    // 如果有识别结果且成功,显示识别结果标签
+    if (imageData.recognitionResult != null &&
+        imageData.recognitionResult!.success &&
+        imageData.recognitionResult!.results.isNotEmpty) {
+      return Container(
+        height: 120,
+        padding: const EdgeInsets.all(8),
+        decoration: BoxDecoration(
+          color: Colors.grey[100],
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: SingleChildScrollView(
+          child: Wrap(
+            spacing: 6,
+            runSpacing: 6,
+            alignment: WrapAlignment.start,
+            crossAxisAlignment: WrapCrossAlignment.start,
+            children: imageData.recognitionResult!.results
+                .map((result) => Container(
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 8,
+                        vertical: 4,
+                      ),
+                      decoration: BoxDecoration(
+                        color: Colors.grey[300],
+                        borderRadius: BorderRadius.circular(12),
+                      ),
+                      child: Text(
+                        result,
+                        style: TextStyle(
+                          fontSize: 12,
+                          color: Colors.grey[800],
+                        ),
+                      ),
+                    ))
+                .toList(),
+          ),
+        ),
+      );
+    }
+    // 如果识别失败或没有识别结果,显示"未识别出异常"或示例图
+    if (imageData.recognitionResult != null &&
+        imageData.recognitionResult!.success &&
+        imageData.recognitionResult!.results.isEmpty) {
+      return Container(
+        height: 120,
+        decoration: BoxDecoration(
+          color: Colors.grey[100],
+          borderRadius: BorderRadius.circular(8),
+        ),
+        child: Center(
+          child: Text(
+            '未识别出异常',
+            style: TextStyle(
+              fontSize: 12,
+              color: Colors.grey[600],
+            ),
+          ),
+        ),
+      );
+    }
+    // 默认显示示例图
+    return Container(
+      height: 120,
+      decoration: BoxDecoration(
+        color: Colors.grey[100],
+        borderRadius: BorderRadius.circular(8),
+      ),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Icon(
+            label == '舌面' || label == '舌下脉络'
+                ? Icons.face
+                : Icons.person,
+            size: 50,
+            color: Colors.grey[400],
+          ),
+          const SizedBox(height: 8),
+          const Text(
+            '示例',
+            style: TextStyle(
+              fontSize: 12,
+              color: Colors.grey,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 底部确定按钮
+  Widget _buildBottomButton() {
+    return Container(
+      padding: const EdgeInsets.all(16),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        boxShadow: [
+          BoxShadow(
+            color: Colors.grey.withOpacity(0.2),
+            spreadRadius: 1,
+            blurRadius: 5,
+            offset: const Offset(0, -3),
+          ),
+        ],
+      ),
+      child: SafeArea(
+        child: SizedBox(
+          width: double.infinity,
+          height: 50,
+          child: ElevatedButton(
+            onPressed: _canSubmit ? _submit : null,
+            style: ElevatedButton.styleFrom(
+              backgroundColor: _canSubmit
+                  ? const Color(0xFF81C784) // 浅绿色
+                  : Colors.grey,
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(25),
+              ),
+            ),
+            child: const Text(
+              '确定',
+              style: TextStyle(
+                fontSize: 16,
+                fontWeight: FontWeight.bold,
+                color: Colors.white,
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 75 - 0
pubspec.yaml

@@ -0,0 +1,75 @@
+name: my_feature_module
+description: "A new Flutter plugin project."
+version: 0.0.1
+homepage:
+
+environment:
+  sdk: '>=3.0.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.2
+  dio: ^5.9.0
+  image_picker: ^1.0.7
+  camera: ^0.10.5+9
+  path_provider: ^2.1.1
+  path: ^1.8.3
+  image: ^4.1.7
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^6.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:
+  # This section identifies this Flutter project as a plugin project.
+  # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
+  # which should be registered in the plugin registry. This is required for
+  # using method channels.
+  # The Android 'package' specifies package in which the registered class is.
+  # This is required for using method channels on Android.
+  # The 'ffiPlugin' specifies that native code should be built and bundled.
+  # This is required for using `dart:ffi`.
+  # All these are used by the tooling to maintain consistency when
+  # adding or updating assets for this project.
+  plugin:
+    platforms:
+      android:
+        package: com.example.my_feature_module
+        pluginClass: MyFeatureModulePlugin
+
+  # To add assets to your plugin package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/to/asset-from-package
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/to/resolution-aware-images
+
+  # To add custom fonts to your plugin package, 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 in packages, see
+  # https://flutter.dev/to/font-from-package

+ 27 - 0
test/my_feature_module_method_channel_test.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:my_feature_module/my_feature_module_method_channel.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  MethodChannelMyFeatureModule platform = MethodChannelMyFeatureModule();
+  const MethodChannel channel = MethodChannel('my_feature_module');
+
+  setUp(() {
+    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
+      channel,
+      (MethodCall methodCall) async {
+        return '42';
+      },
+    );
+  });
+
+  tearDown(() {
+    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
+  });
+
+  test('getPlatformVersion', () async {
+    expect(await platform.getPlatformVersion(), '42');
+  });
+}

+ 27 - 0
test/my_feature_module_test.dart

@@ -0,0 +1,27 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:my_feature_module/my_feature_module.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+class MockMyFeatureModulePlatform
+    with MockPlatformInterfaceMixin
+    implements MyFeatureModulePlatform {
+
+  @override
+  Future<String?> getPlatformVersion() => Future.value('42');
+}
+
+void main() {
+  final MyFeatureModulePlatform initialPlatform = MyFeatureModulePlatform.instance;
+
+  test('$MethodChannelMyFeatureModule is the default instance', () {
+    expect(initialPlatform, isInstanceOf<MethodChannelMyFeatureModule>());
+  });
+
+  test('getPlatformVersion', () async {
+    MyFeatureModule myFeatureModulePlugin = MyFeatureModule();
+    MockMyFeatureModulePlatform fakePlatform = MockMyFeatureModulePlatform();
+    MyFeatureModulePlatform.instance = fakePlatform;
+
+    expect(await myFeatureModulePlugin.getPlatformVersion(), '42');
+  });
+}