소스 검색

[new]增加客服入口

zk 8 달 전
부모
커밋
ee5e51accf
53개의 변경된 파일2788개의 추가작업 그리고 13개의 파일을 삭제
  1. 28 0
      android/app/proguard-rules.pro
  2. 3 3
      android/app/src/main/AndroidManifest.xml
  3. 3 1
      android/build.gradle
  4. 2 2
      lib/data/api/response/request_friend_list_response.g.dart
  5. 1 2
      lib/data/bean/message_info.g.dart
  6. 2 0
      lib/data/consts/build_config.dart
  7. 4 0
      lib/data/repositories/account_repository.dart
  8. 1 1
      lib/di/get_it.config.dart
  9. 5 2
      lib/main.dart
  10. 5 1
      lib/module/feedback/feed_back_controller.dart
  11. 4 1
      lib/module/mine/mine_controller.dart
  12. 87 0
      lib/sdk/qiyu/qi_yu_helper.dart
  13. 27 0
      lib/sdk/qiyu/qiyu_info_bean.dart
  14. 22 0
      lib/sdk/qiyu/qiyu_info_bean.g.dart
  15. 24 0
      plugins/flutter_qiyu/CHANGELOG.md
  16. 21 0
      plugins/flutter_qiyu/LICENSE
  17. 194 0
      plugins/flutter_qiyu/README.md
  18. 4 0
      plugins/flutter_qiyu/analysis_options.yaml
  19. 67 0
      plugins/flutter_qiyu/android/build.gradle
  20. 4 0
      plugins/flutter_qiyu/android/gradle.properties
  21. BIN
      plugins/flutter_qiyu/android/gradle/wrapper/gradle-wrapper.jar
  22. 7 0
      plugins/flutter_qiyu/android/gradle/wrapper/gradle-wrapper.properties
  23. 249 0
      plugins/flutter_qiyu/android/gradlew
  24. 92 0
      plugins/flutter_qiyu/android/gradlew.bat
  25. 10 0
      plugins/flutter_qiyu/android/local.properties
  26. 1 0
      plugins/flutter_qiyu/android/settings.gradle
  27. 2 0
      plugins/flutter_qiyu/android/src/main/AndroidManifest.xml
  28. 49 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/ActivityForResultUtil.java
  29. 325 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/DemoRequestPermissionEvent.java
  30. 350 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/FlutterQiyuPlugin.java
  31. 27 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/GlideGifImagerLoader.java
  32. 48 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/GlideImageLoader.java
  33. 38 0
      plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/QiYuUtils.java
  34. 5 0
      plugins/flutter_qiyu/android/src/main/res/drawable/blue_button_background.xml
  35. 5 0
      plugins/flutter_qiyu/android/src/main/res/drawable/dialog_background.xml
  36. 9 0
      plugins/flutter_qiyu/android/src/main/res/drawable/ic_close.xml
  37. 50 0
      plugins/flutter_qiyu/android/src/main/res/layout/permission_dialog.xml
  38. 17 0
      plugins/flutter_qiyu/android/src/main/res/layout/permission_tip_view.xml
  39. 9 0
      plugins/flutter_qiyu/ios/Classes/FlutterQiyuPlugin.h
  40. 349 0
      plugins/flutter_qiyu/ios/Classes/FlutterQiyuPlugin.m
  41. 38 0
      plugins/flutter_qiyu/ios/Classes/UIBarButtonItem+blocks.h
  42. 62 0
      plugins/flutter_qiyu/ios/Classes/UIBarButtonItem+blocks.m
  43. 26 0
      plugins/flutter_qiyu/ios/flutter_qiyu.podspec
  44. 5 0
      plugins/flutter_qiyu/lib/flutter_qiyu.dart
  45. 98 0
      plugins/flutter_qiyu/lib/qiyu.dart
  46. 47 0
      plugins/flutter_qiyu/lib/qy_commodity_info.dart
  47. 70 0
      plugins/flutter_qiyu/lib/qy_service_window_params.dart
  48. 27 0
      plugins/flutter_qiyu/lib/qy_source.dart
  49. 23 0
      plugins/flutter_qiyu/lib/qy_user_info_params.dart
  50. 205 0
      plugins/flutter_qiyu/pubspec.lock
  51. 26 0
      plugins/flutter_qiyu/pubspec.yaml
  52. 7 0
      pubspec.lock
  53. 4 0
      pubspec.yaml

+ 28 - 0
android/app/proguard-rules.pro

@@ -119,6 +119,34 @@ public <methods>;
 
 #oaid miitmdid end
 
+
+# AMAP start
+-keep class com.amap.api.maps.**{*;}
+-keep class com.autonavi.amap.mapcore.*{*;}
+-keep class com.amap.api.trace.**{*;}
+-keep class com.amap.api.maps.**{*;}
+-keep class com.autonavi.**{*;}
+-keep class com.amap.api.trace.**{*;}
+-keep class com.amap.api.location.**{*;}
+-keep class com.amap.api.fence.**{*;}
+-keep class com.loc.**{*;}
+-keep class com.autonavi.aps.amapapi.model.**{*;}
+-keep class com.amap.api.services.**{*;}
+-keep class com.amap.api.maps2d.**{*;}
+-keep class com.amap.api.mapcore2d.**{*;}
+-keep class com.amap.api.navi.**{*;}
+-keep class com.autonavi.**{*;}
+# AMAP end
+
+#七鱼客服
+-dontwarn com.qiyukf.**
+-keep class com.qiyukf.** {*;}
+-dontwarn com.netease.**
+-keep class com.netease.** {*;}
+-dontwarn org.slf4j.**
+-keep class org.slf4j.** { *; }
+
+
 #flutter start
 
 -keep class io.flutter.** { *; }

+ 3 - 3
android/app/src/main/AndroidManifest.xml

@@ -21,10 +21,10 @@
         <activity
             android:name=".MainActivity"
             android:exported="true"
-            android:launchMode="singleTop"
-            android:taskAffinity=""
+            android:launchMode="singleTask"
+            android:taskAffinity="${applicationId}"
             android:theme="@style/LaunchTheme"
-            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+            android:screenOrientation="portrait"
             android:hardwareAccelerated="true"
             android:windowSoftInputMode="adjustResize">
             <!-- Specifies an Android theme to apply to this Activity as soon as

+ 3 - 1
android/build.gradle

@@ -6,11 +6,13 @@ allprojects {
         targetSdkVersion = 34
     }
     repositories {
-        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
+
+//        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
         maven { url 'https://maven.aliyun.com/repository/google' }
         maven { url 'https://maven.aliyun.com/repository/public' }
         maven { url 'https://maven.aliyun.com/repository/central' }
         maven { url 'https://repo.huaweicloud.com/repository/maven/' }
+        mavenCentral()
 
         maven {
             credentials {

+ 2 - 2
lib/data/api/response/request_friend_list_response.g.dart

@@ -7,7 +7,7 @@ part of 'request_friend_list_response.dart';
 // **************************************************************************
 
 RequestFriendListResponse _$RequestFriendListResponseFromJson(
-    Map<String, dynamic> json) =>
+        Map<String, dynamic> json) =>
     RequestFriendListResponse(
       (json['count'] as num).toInt(),
       (json['list'] as List<dynamic>?)
@@ -16,7 +16,7 @@ RequestFriendListResponse _$RequestFriendListResponseFromJson(
     );
 
 Map<String, dynamic> _$RequestFriendListResponseToJson(
-    RequestFriendListResponse instance) =>
+        RequestFriendListResponse instance) =>
     <String, dynamic>{
       'count': instance.count,
       'list': instance.list,

+ 1 - 2
lib/data/bean/message_info.g.dart

@@ -6,8 +6,7 @@ part of 'message_info.dart';
 // JsonSerializableGenerator
 // **************************************************************************
 
-MessageInfo _$MessageInfoFromJson(Map<String, dynamic> json) =>
-    MessageInfo(
+MessageInfo _$MessageInfoFromJson(Map<String, dynamic> json) => MessageInfo(
       id: (json['id'] as num).toInt(),
       type: (json['type'] as num).toInt(),
       senderId: json['senderId'] as String,

+ 2 - 0
lib/data/consts/build_config.dart

@@ -6,4 +6,6 @@ final class BuildConfig {
   static bool get isDebug => kDebugMode;
 
   static final String wechatAppId = 'wx64695e2b8346a227';
+
+  static const String qiyuKEY = "09ea6e0a6d006e25462906fbf6758c99";
 }

+ 4 - 0
lib/data/repositories/account_repository.dart

@@ -22,6 +22,7 @@ import 'package:location/utils/atmob_log.dart';
 import 'package:location/utils/http_handler.dart';
 import 'package:location/utils/mmkv_util.dart';
 import '../../sdk/map/map_helper.dart';
+import '../../sdk/qiyu/qi_yu_helper.dart';
 import '../api/response/login_response.dart';
 
 @lazySingleton
@@ -142,6 +143,8 @@ class AccountRepository {
     friendsRepository.clearFriends();
     messageRepository.clearMessage();
     urgentContactRepository.clearContactList();
+
+    QiYuHelper.logout();
   }
 
   void refreshMemberStatus() {
@@ -167,6 +170,7 @@ class AccountRepository {
         .then((response) {
       refreshMemberHandler?.cancel();
       if (response != null) {
+        QiYuHelper.setUserInfo(loginPhoneNum.value, response.userId);
         KVUtil.putString(keyAccountLoginUserId, response.userId);
         if (!response.permanent && !response.expired) {
           refreshMemberHandler = Timer(

+ 1 - 1
lib/di/get_it.config.dart

@@ -52,9 +52,9 @@ extension GetItInjectableX on _i174.GetIt {
     );
     final networkModule = _$NetworkModule();
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
+    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.factory<_i269.MemberController>(() => _i269.MemberController());
     gh.factory<_i973.SplashController>(() => _i973.SplashController());
-    gh.factory<_i769.FeedBackController>(() => _i769.FeedBackController());
     gh.singleton<_i361.Dio>(() => networkModule.createDefaultDio());
     gh.lazySingleton<_i220.AtmobLocationClient>(
         () => _i220.AtmobLocationClient());

+ 5 - 2
lib/main.dart

@@ -11,6 +11,7 @@ import 'package:location/resource/string.gen.dart';
 import 'package:location/resource/string_source.dart';
 import 'package:location/router/app_pages.dart';
 import 'package:location/sdk/map/map_helper.dart';
+import 'package:location/sdk/qiyu/qi_yu_helper.dart';
 import 'package:location/sdk/wechat/wechat_helper.dart';
 import 'package:location/utils/app_info_util.dart';
 import 'package:location/utils/mmkv_util.dart';
@@ -32,11 +33,11 @@ void main() async {
   //非隐私相关
   initCommon();
 
+  runApp(const MyApp());
+
   //隐私相关:系统参数&第三方sdk初始化
   await PrivacyCompliance.ensurePolicyGranted(AppInitTask());
 
-  runApp(const MyApp());
-
   //檢查地址
   checkEnv();
 }
@@ -67,6 +68,8 @@ class AppInitTask implements EnsurePolicyGrant {
     await deviceInfoUtil.init();
     //初始化其他sdk
     await MapHelper.init();
+
+    QiYuHelper.init();
   }
 }
 

+ 5 - 1
lib/module/feedback/feed_back_controller.dart

@@ -5,6 +5,8 @@ import 'package:location/base/base_controller.dart';
 import 'package:location/resource/string.gen.dart';
 import 'package:location/utils/toast_util.dart';
 
+import '../../sdk/qiyu/qi_yu_helper.dart';
+
 @injectable
 class FeedBackController extends BaseController {
   final RxString _content = RxString('');
@@ -28,5 +30,7 @@ class FeedBackController extends BaseController {
     Get.back();
   }
 
-  void onCustomerServiceClick() {}
+  void onCustomerServiceClick() {
+    QiYuHelper.openCustomService();
+  }
 }

+ 4 - 1
lib/module/mine/mine_controller.dart

@@ -9,6 +9,7 @@ import 'package:location/module/urgent_contact/urgent_contact_page.dart';
 import 'package:location/resource/string.gen.dart';
 import '../../data/repositories/account_repository.dart';
 import '../../dialog/common_alert_dialog_impl.dart';
+import '../../sdk/qiyu/qi_yu_helper.dart';
 import '../../sdk/wechat/wechat_share_util.dart';
 import '../../utils/toast_util.dart';
 
@@ -42,7 +43,9 @@ class MineController extends BaseController {
     });
   }
 
-  onCustomerServiceClick() {}
+  onCustomerServiceClick() {
+    QiYuHelper.openCustomService();
+  }
 
   onPermissionSettingClick() {}
 

+ 87 - 0
lib/sdk/qiyu/qi_yu_helper.dart

@@ -0,0 +1,87 @@
+import 'dart:convert';
+
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_qiyu/qiyu.dart';
+import 'package:flutter_qiyu/qy_service_window_params.dart';
+import 'package:flutter_qiyu/qy_user_info_params.dart';
+import 'package:location/data/consts/build_config.dart';
+import 'package:location/sdk/qiyu/qiyu_info_bean.dart';
+
+import '../../resource/string.gen.dart';
+import '../../utils/app_info_util.dart';
+import '../../utils/atmob_log.dart';
+
+class QiYuHelper {
+  static String tag = 'QiYuHelper';
+
+  QiYuHelper._();
+
+  static void init() async {
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      QiYu.registerApp(
+        appKey: BuildConfig.qiyuKEY,
+        appName: StringName.appName,
+      );
+    });
+  }
+
+  static void openCustomService() {
+    QYServiceWindowParams serviceWindowParams = QYServiceWindowParams.fromJson({
+      'source': {'sourceTitle': '', 'sourceUrl': '', 'sourceCustomInfo': ''},
+      'sessionTitle': '客服',
+      'groupId': 0,
+      'staffId': 0,
+      'robotId': 0,
+      'robotFirst': false,
+      'faqTemplateId': 0,
+      'vipLevel': 0,
+      'showQuitQueue': true,
+      'showCloseSessionEntry': true
+    });
+    QiYu.openServiceWindow(serviceWindowParams);
+  }
+
+  static void setUserInfo(String? phone, String serverUserId) async {
+    List<QiYuInfoBean> data = [
+      QiYuInfoBean(
+        index: '1',
+        key: 'serverUserId',
+        label: '用户Id',
+        value: serverUserId,
+      ),
+      QiYuInfoBean(
+        index: '2',
+        key: 'version',
+        label: '应用版本',
+        value: appInfoUtil.appVersionName,
+      ),
+      QiYuInfoBean(
+        index: '3',
+        key: 'app_name',
+        label: '应用名称',
+        value: appInfoUtil.appName,
+      ),
+      QiYuInfoBean(
+        index: '4',
+        key: 'packageName',
+        label: '包名',
+        value: appInfoUtil.packageName,
+      ),
+    ];
+    String dataJson = jsonEncode(data.map((e) => e.toJson()).toList());
+    AtmobLog.d(tag, 'setUserInfo dataJson: $dataJson');
+    QYUserInfoParams userInfoParams =
+        QYUserInfoParams.fromJson({'userId': phone, 'data': dataJson});
+    try {
+      await QiYu.setUserInfo(userInfoParams);
+      AtmobLog.d(tag, 'setUserInfo success');
+    } catch (error) {
+      AtmobLog.e(tag, 'setUserInfo error: $error');
+    }
+  }
+
+  static void logout() async {
+    await QiYu.logout();
+    AtmobLog.d(tag, 'logout');
+  }
+}

+ 27 - 0
lib/sdk/qiyu/qiyu_info_bean.dart

@@ -0,0 +1,27 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'qiyu_info_bean.g.dart';
+
+@JsonSerializable()
+class QiYuInfoBean {
+  @JsonKey(name: 'index')
+  String? index;
+  @JsonKey(name: 'key')
+  String? key;
+  @JsonKey(name: 'label')
+  String? label;
+  @JsonKey(name: 'value')
+  String? value;
+
+  QiYuInfoBean({
+    this.index,
+    this.key,
+    this.label,
+    this.value,
+  });
+
+  factory QiYuInfoBean.fromJson(Map<String, dynamic> json) =>
+      _$QiYuInfoBeanFromJson(json);
+
+  Map<String, dynamic> toJson() => _$QiYuInfoBeanToJson(this);
+}

+ 22 - 0
lib/sdk/qiyu/qiyu_info_bean.g.dart

@@ -0,0 +1,22 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'qiyu_info_bean.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+QiYuInfoBean _$QiYuInfoBeanFromJson(Map<String, dynamic> json) => QiYuInfoBean(
+      index: json['index'] as String?,
+      key: json['key'] as String?,
+      label: json['label'] as String?,
+      value: json['value'] as String?,
+    );
+
+Map<String, dynamic> _$QiYuInfoBeanToJson(QiYuInfoBean instance) =>
+    <String, dynamic>{
+      'index': instance.index,
+      'key': instance.key,
+      'label': instance.label,
+      'value': instance.value,
+    };

+ 24 - 0
plugins/flutter_qiyu/CHANGELOG.md

@@ -0,0 +1,24 @@
+## 0.1.2
+
+- 更换新版七鱼sdk #16
+
+## 0.1.1
+
+- 修复 Android 端 `channel.invokeMethod` 未初始化完成前被调用的问题
+
+## 0.1.0
+
+- [#7](https://github.com/leanflutter/flutter_qiyu/issues/7) 修复 iOS 端编译错误问题
+
+## 0.0.3
+
+- [#6](https://github.com/leanflutter/flutter_qiyu/issues/6) 修复 Android 端编译错误的问题
+
+## 0.0.2
+
+- 空安全支持
+- [#4](https://github.com/leanflutter/flutter_qiyu/issues/4) 修复程序包com.netease.nimlib.sdk不存在的问题
+
+## 0.0.1
+
+- first release

+ 21 - 0
plugins/flutter_qiyu/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-2014 LiJianying <lijy91@foxmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 194 - 0
plugins/flutter_qiyu/README.md


+ 4 - 0
plugins/flutter_qiyu/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

+ 67 - 0
plugins/flutter_qiyu/android/build.gradle

@@ -0,0 +1,67 @@
+group 'org.leanflutter.plugins.flutter_qiyu'
+version '1.0'
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:7.3.0'
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+
+// 加载 local.properties 文件
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withInputStream { stream ->
+        localProperties.load(stream)
+    }
+}
+
+// 读取变量
+def flutterSdk = localProperties.getProperty('flutter.sdk')
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 31
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    defaultConfig {
+        minSdkVersion 16
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
+
+dependencies {
+    //flutter
+    compileOnly files("$flutterSdk/bin/cache/artifacts/engine/android-arm/flutter.jar")
+
+    //AndroidX
+    compileOnly "androidx.annotation:annotation:1.1.0"
+    compileOnly 'androidx.activity:activity:1.6.1'
+
+    implementation "androidx.constraintlayout:constraintlayout:2.1.1"
+    implementation 'com.qiyukf.unicorn:unicorn:+'
+    implementation 'com.github.bumptech.glide:glide:4.10.0'
+//    annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
+//    implementation 'com.github.getActivity:XXPermissions:20.0'
+}

+ 4 - 0
plugins/flutter_qiyu/android/gradle.properties

@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true

BIN
plugins/flutter_qiyu/android/gradle/wrapper/gradle-wrapper.jar


+ 7 - 0
plugins/flutter_qiyu/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 249 - 0
plugins/flutter_qiyu/android/gradlew

@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 92 - 0
plugins/flutter_qiyu/android/gradlew.bat

@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 10 - 0
plugins/flutter_qiyu/android/local.properties

@@ -0,0 +1,10 @@
+## This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Mon Jan 13 11:50:22 CST 2025
+sdk.dir=D\:\\Android\\SDK
+flutter.sdk=D\:\\Flutter\\FlutterSDK
+

+ 1 - 0
plugins/flutter_qiyu/android/settings.gradle

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

+ 2 - 0
plugins/flutter_qiyu/android/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.leanflutter.plugins.flutter_qiyu"></manifest>

+ 49 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/ActivityForResultUtil.java

@@ -0,0 +1,49 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+
+import androidx.activity.ComponentActivity;
+import androidx.activity.result.ActivityResultCallback;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.ActivityResultRegistry;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.UUID;
+
+public class ActivityForResultUtil<I, O> {
+
+
+    ActivityResultLauncher<I> launcher = null;
+
+
+    public void startActivityForResult(ComponentActivity activity, ActivityResultContract<I, O> contract, I input, ActivityResultCallback<O> callback) {
+        String key = UUID.randomUUID().toString();
+
+        ActivityResultRegistry registry = activity.getActivityResultRegistry();
+        LifecycleEventObserver observer = new LifecycleEventObserver() {
+
+            @Override
+            public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) {
+                if (Lifecycle.Event.ON_DESTROY == event) {
+                    if (launcher != null) {
+                        launcher.unregister();
+                    }
+                    activity.getLifecycle().removeObserver(this);
+                }
+            }
+        };
+        activity.getLifecycle().addObserver(observer);
+
+        launcher = registry.register(key, contract, result -> {
+            if (launcher != null) {
+                launcher.unregister();
+            }
+            activity.getLifecycle().removeObserver(observer);
+            callback.onActivityResult(result);
+        });
+        launcher.launch(input);
+    }
+}

+ 325 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/DemoRequestPermissionEvent.java

@@ -0,0 +1,325 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+import android.Manifest;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.PixelFormat;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.Gravity;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.activity.ComponentActivity;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import com.qiyukf.unicorn.api.event.EventCallback;
+import com.qiyukf.unicorn.api.event.UnicornEventBase;
+import com.qiyukf.unicorn.api.event.entry.RequestPermissionEventEntry;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import android.content.pm.PackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback;
+
+import android.app.Activity;
+
+public class DemoRequestPermissionEvent implements UnicornEventBase<RequestPermissionEventEntry> {
+    private final FlutterQiyuPlugin plugin;
+    private final Context context;
+    private Map<String, String> h5MessageHandlerMap = new HashMap<>();
+    private View tipView;
+    private WindowManager windowManager;
+    private boolean isTipVisible = false;
+    private static final int REQUEST_CODE = 100;
+
+    public DemoRequestPermissionEvent(Context context, FlutterQiyuPlugin plugin) {
+        this.context = context;
+        this.plugin = plugin;
+
+        // 初始化权限映射
+        h5MessageHandlerMap.put("android.permission.RECORD_AUDIO", "麦克风");
+        h5MessageHandlerMap.put("android.permission.CAMERA", "相机");
+        h5MessageHandlerMap.put("android.permission.READ_EXTERNAL_STORAGE", "存储");
+        h5MessageHandlerMap.put("android.permission.WRITE_EXTERNAL_STORAGE", "存储");
+        h5MessageHandlerMap.put("android.permission.READ_MEDIA_AUDIO", "多媒体文件");
+        h5MessageHandlerMap.put("android.permission.READ_MEDIA_IMAGES", "多媒体文件");
+        h5MessageHandlerMap.put("android.permission.READ_MEDIA_VIDEO", "多媒体文件");
+        h5MessageHandlerMap.put("android.permission.POST_NOTIFICATIONS", "通知栏权限");
+    }
+
+    private String transToPermissionStr(List<String> permissionList) {
+        if (permissionList == null || permissionList.size() == 0) {
+            return "";
+        }
+        HashSet<String> set = new HashSet<>();
+        for (int i = 0; i < permissionList.size(); i++) {
+            if (!TextUtils.isEmpty(h5MessageHandlerMap.get(permissionList.get(i)))) {
+                set.add(h5MessageHandlerMap.get(permissionList.get(i)));
+            }
+        }
+        if (set.isEmpty()) {
+            return "";
+        }
+        StringBuilder str = new StringBuilder();
+        for (String temp : set) {
+            str.append(temp);
+            str.append("、");
+        }
+        if (str.length() > 0) {
+            str.deleteCharAt(str.length() - 1);
+        }
+        return str.toString();
+    }
+
+    private void showPermissionTip(Context context, String permission) {
+        if (windowManager == null) {
+            windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        }
+
+        // 如果已经有提示在显示,先移除
+        removeTipView();
+
+        // 创建提示视图
+        tipView = LayoutInflater.from(context).inflate(R.layout.permission_tip_view, null);
+        
+        // 从 tipView 中找到 TextView
+        TextView tipText = tipView.findViewById(R.id.tip_text);
+
+        // 设置提示文本
+        tipText.setText(getTipText(permission));
+
+        // 设置布局参数
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
+        params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
+        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+        params.format = PixelFormat.TRANSLUCENT;
+        params.width = WindowManager.LayoutParams.MATCH_PARENT;
+        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+
+        // 设置 y 值为 50dp
+        params.gravity = Gravity.TOP;
+        params.y = (int) (50 * context.getResources().getDisplayMetrics().density); // 设置 y 值为 50dp
+
+        // 显示提示
+        try {
+            windowManager.addView(tipView, params);
+            isTipVisible = true; // 更新标志
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void removeTipView() {
+        if (tipView != null && windowManager != null) {
+            try {
+                windowManager.removeView(tipView);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            tipView = null;
+        }
+    }
+
+    private String getTipText(String permission) {
+        if (permission.contains("CAMERA")) {
+            return "使用该功能需要使用 \"开启相机权限\",开启相机权限后,授权后您可以使用拍摄照片发送图片与在线客服联系并协助您解答疑惑\"等功能";
+        } else if (permission.contains("RECORD_AUDIO")) {
+            return "使用该功能需要使用 \"麦克风权限\",开启麦克风权限后,用于发送语音,录制有声视频,与在线客服联系并协助您解答疑惑";
+        } else if (permission.contains("READ_EXTERNAL_STORAGE") || permission.contains("WRITE_EXTERNAL_STORAGE")) {
+            return "使用该功能需要使用 \"存储权限\",开启存储权限后,用于保存文件。";
+        } else if (permission.contains("READ_MEDIA_AUDIO") || permission.contains("READ_MEDIA_IMAGES") || permission.contains("READ_MEDIA_VIDEO")) {
+            return "使用该功能需要使用 \"存储权限\",开启存储权限后,用于发送文件,与在线客服联系并协助您解答疑惑";
+        } else if (permission.contains("POST_NOTIFICATIONS")) {
+            return "使用该功能需要使用 \"通知权限\",开启通知权限后,您可以及时收到客服消息提醒。";
+        } else {
+            return "开启权限后,您可以使用相关功能。";
+        }
+    }
+
+    @Override
+    public void onEvent(RequestPermissionEventEntry entry, Context context, EventCallback<RequestPermissionEventEntry> callback) {
+        List<String> permissions = entry.getPermissionList();
+        for (String permission : permissions) {
+            showPermissionDialog(context, entry.getScenesType(), permission, entry, callback);
+        }
+    }
+
+    private void showPermissionDialog(Context context, int type, String permissionName,
+                                      RequestPermissionEventEntry entry, EventCallback<RequestPermissionEventEntry> callback) {
+        Dialog dialog = new Dialog(context);
+        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        View dialogView = LayoutInflater.from(context).inflate(R.layout.permission_dialog, null);
+        dialog.setContentView(dialogView);
+
+        // 设置消息
+        TextView messageView = dialogView.findViewById(R.id.message);
+        messageView.setText(getPermissionMessage(type, permissionName));
+
+        // 设置关闭按钮
+        ImageView closeButton = dialogView.findViewById(R.id.close_button);
+        closeButton.setOnClickListener(v -> dialog.dismiss());
+
+        // 设置确认按钮
+        Button confirmButton = dialogView.findViewById(R.id.confirm_button);
+        confirmButton.setOnClickListener(v -> {
+
+
+            // 显示权限提示
+            List<String> permissions = entry.getPermissionList();
+            if (!permissions.isEmpty()) {
+                showPermissionTip(context, permissions.get(0));
+
+            }
+
+            if (context instanceof ComponentActivity) {
+                for (String permission : permissions) {
+                    new ActivityForResultUtil<String, Boolean>().startActivityForResult((ComponentActivity) context, new ActivityResultContracts.RequestPermission(), permission, result -> {
+                        Log.d("qqq", "result: " + result);
+                        if (isTipVisible) {
+                            removeTipView();
+                            isTipVisible = false;
+                        }
+                        if (result) {
+                            dialog.dismiss();
+                            callback.onProcessEventSuccess(entry);
+                        } else {
+                            dialog.dismiss();
+                        }
+                    });
+                }
+            }
+        });
+
+        // 设置对话框宽度
+        Window window = dialog.getWindow();
+        if (window != null) {
+            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+            WindowManager.LayoutParams params = window.getAttributes();
+            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+            window.setAttributes(params);
+        }
+
+
+        dialog.show();
+    }
+
+    private String getPermissionMessage(int type, String permissionName) {
+        switch (type) {
+            case 4: // 视频
+                return "使用该功能需要使用 \"存储权限\",启用该权限后,用于发送视频文件,与在线客服联系并协助您解答疑惑";
+            case 2:
+                return "使用该功能需要使用 \"存储权限\",开启存储权限后,用于保存文件。";
+            case 0:
+            case 5: // 存储
+                return "使用该功能需要使用 \"存储权限\",开启存储权限后,用于发送文件,与在线客服联系并协助您解答疑惑";
+            case 6: // 相册
+                return "使用该功能需要使用 \"相册权限\",用于发送图片文件,与在线客服联系并协助您解答疑惑";
+            case 1: // 拍摄视频
+                return "使用该功能需要使用 \"开启相机权限和录音权限\",开启该权限后,授权后您可以拍摄视频,用于发送视频与在线客服联系并协助您解答疑惑等功能";
+            case 7: // 拍照
+                return "使用该功能需要使用 \"开启相机权限\",开启相机权限后,授权后您可以使用拍摄照片用于发送图片与在线客服联系并协助您解答疑惑等功能";
+            case 8: // 录音
+                return "使用该功能需要使用 \"麦克风权限\",开启麦克风权限后,用于发送语音,与在线客服联系并协助您解答疑惑";
+            case 10: // 通知
+                return "使用该功能需要使用 \"通知权限\",开启通知权限后,您可以及时收到客服消息提醒。";
+            default:
+                return "使用该功能需要使用 \"" + permissionName + "\",开启权限后才能正常使用该功能。";
+        }
+    }
+
+    @Override
+    public boolean onDenyEvent(Context context, RequestPermissionEventEntry entry) {
+        if (isTipVisible) {
+            removeTipView();
+            isTipVisible = false;
+        }
+        String permissionName = transToPermissionStr(entry.getPermissionList());
+
+        Dialog dialog = new Dialog(context);
+        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        View dialogView = LayoutInflater.from(context).inflate(R.layout.permission_dialog, null);
+        dialog.setContentView(dialogView);
+
+        // 设置消息
+        TextView messageView = dialogView.findViewById(R.id.message);
+        messageView.setText("您没有开启\"" + (TextUtils.isEmpty(permissionName) ? "相关" : permissionName)
+                + "\"权限,是否前往设置开启?");
+
+        // 设置关闭按钮
+        ImageView closeButton = dialogView.findViewById(R.id.close_button);
+        closeButton.setOnClickListener(v -> dialog.dismiss());
+
+        // 设置确认按钮
+        Button confirmButton = dialogView.findViewById(R.id.confirm_button);
+        confirmButton.setText("去设置");
+        confirmButton.setOnClickListener(v -> {
+            List<String> permissions = entry.getPermissionList();
+            for (String permission : permissions) {
+
+            }
+            dialog.dismiss();
+        });
+
+        // 设置对话框宽度
+        Window window = dialog.getWindow();
+        if (window != null) {
+            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+            WindowManager.LayoutParams params = window.getAttributes();
+            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+            window.setAttributes(params);
+        }
+
+        dialog.setOnDismissListener(dialogInterface -> {
+            // 当对话框关闭时,检查是否需要移除提示
+            if (isTipVisible) {
+                removeTipView();
+                isTipVisible = false;
+            }
+        });
+
+        dialog.show();
+        return true;
+    }
+
+    // 在适当的时机(比如 Activity 销毁时)清理资源
+    public boolean cleanUp() {
+        try {
+            removeTipView(); // 尝试移除提示视图
+            Log.d("qqq", "cleanUp successful");
+            return true; // 清理成功
+        } catch (Exception e) {
+            Log.e("qqq", "cleanUp failed", e);
+            return false; // 清理失败
+        }
+    }
+}
+

+ 350 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/FlutterQiyuPlugin.java

@@ -0,0 +1,350 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.qiyukf.nimlib.sdk.RequestCallback;
+import com.qiyukf.nimlib.sdk.StatusBarNotificationConfig;
+import com.qiyukf.unicorn.api.ConsultSource;
+import com.qiyukf.unicorn.api.OnBotEventListener;
+import com.qiyukf.unicorn.api.ProductDetail;
+import com.qiyukf.unicorn.api.UICustomization;
+import com.qiyukf.unicorn.api.Unicorn;
+import com.qiyukf.unicorn.api.UnreadCountChangeListener;
+import com.qiyukf.unicorn.api.YSFOptions;
+import com.qiyukf.unicorn.api.YSFUserInfo;
+import com.qiyukf.unicorn.api.lifecycle.SessionLifeCycleOptions;
+import com.qiyukf.unicorn.api.event.SDKEvents;
+import com.qiyukf.unicorn.api.event.EventProcessFactory;
+import com.qiyukf.unicorn.api.event.UnicornEventBase;
+import com.qiyukf.unicorn.api.event.entry.RequestPermissionEventEntry;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.BinaryMessenger;
+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;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
+
+/**
+ * FlutterQiyuPlugin
+ */
+public class FlutterQiyuPlugin implements FlutterPlugin, MethodCallHandler {
+    private static final String CHANNEL_NAME = "flutter_qiyu";
+    private static FlutterQiyuPlugin instance;
+    
+    // 定义权限请求事件类型常量
+    private static final int EVENT_TYPE_REQUEST_PERMISSION = 5;
+
+    public FlutterQiyuPlugin() {
+        instance = this;
+    }
+
+    public static void config(Context context, String appKey) {
+        YSFOptions ysfOptions = new YSFOptions();
+        ysfOptions.statusBarNotificationConfig = new StatusBarNotificationConfig();
+        
+        // 添加 SDK 事件处理
+        ysfOptions.sdkEvents = new SDKEvents();
+        ysfOptions.sdkEvents.eventProcessFactory = new EventProcessFactory() {
+            @Override
+            public UnicornEventBase eventOf(int eventType) {
+                if (eventType == EVENT_TYPE_REQUEST_PERMISSION) {
+                    return new DemoRequestPermissionEvent(context, instance);
+                }
+                return null;
+            }
+        };
+
+        ysfOptions.onBotEventListener = new OnBotEventListener() {
+            @Override
+            public boolean onUrlClick(Context context, String url) {
+                Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+                context.startActivity(intent);
+                return true;
+            }
+        };
+        
+
+        // 如果项目中使用了 Glide 可以通过设置 gifImageLoader 去加载 gif 图片
+        ysfOptions.gifImageLoader = new GlideGifImagerLoader(context);
+
+        Unicorn.config(context.getApplicationContext(), appKey, ysfOptions, new GlideImageLoader(context));
+    }
+
+    /// 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 MethodChannel channel;
+
+    private Context context;
+    private YSFOptions ysfOptions;
+    private UnreadCountChangeListener unreadCountChangeListener = new UnreadCountChangeListener() {
+        @Override
+        public void onUnreadCountChange(int unreadCount) {
+            if (channel == null) return;
+
+            Map<String, Object> map = new HashMap<>();
+            map.put("unreadCount", unreadCount);
+
+            channel.invokeMethod("onUnreadCountChange", map);
+        }
+    };
+
+    private DemoRequestPermissionEvent demoRequestPermissionEvent;
+
+    @Override
+    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+        this.setupChannel(flutterPluginBinding.getBinaryMessenger(), flutterPluginBinding.getApplicationContext());
+        demoRequestPermissionEvent = new DemoRequestPermissionEvent(flutterPluginBinding.getApplicationContext(), this);
+    }
+
+    @Override
+    public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
+        this.teardownChannel();
+    }
+
+    @Override
+    public void onMethodCall(MethodCall call, Result result) {
+        if (call.method.equals("getPlatformVersion")) {
+            result.success("Android " + android.os.Build.VERSION.RELEASE);
+        } else if (call.method.equals("registerApp")) {
+            String appKey = call.argument("appKey");
+            String appName = call.argument("appName");
+
+            this.registerApp(appKey, appName);
+            result.success(true);
+        } else if (call.method.equals("openServiceWindow")) {
+            this.openServiceWindow(call);
+            result.success(true);
+        } else if (call.method.equals("setCustomUIConfig")) {
+            this.setCustomUIConfig(call);
+            result.success(true);
+        } else if (call.method.equals("getUnreadCount")) {
+            this.getUnreadCount(call, result);
+        } else if (call.method.equals("setUserInfo")) {
+            this.setUserInfo(call, result);
+        } else if (call.method.equals("logout")) {
+            this.logout();
+        } else if (call.method.equals("cleanCache")) {
+            this.cleanCache();
+        } else if (call.method.equals("cleanUp")) {
+            if (demoRequestPermissionEvent != null) {
+                boolean success = demoRequestPermissionEvent.cleanUp();
+                result.success(success);
+            } else {
+                result.success(false);
+            }
+        } else {
+            result.notImplemented();
+        }
+    }
+
+    private void registerApp(String appKey, String appName) {
+        Unicorn.initSdk();
+        config(context, appKey);
+        Unicorn.addUnreadCountChangeListener(unreadCountChangeListener, true);
+    }
+
+    private void openServiceWindow(MethodCall call) {
+        Map sourceMap = call.argument("source");
+        Map commodityInfoMap = call.argument("commodityInfo");
+
+        String sourceTitle = (String) sourceMap.get("sourceTitle");
+        String sourceUrl = (String) sourceMap.get("sourceUrl");
+        String sourceCustomInfo = (String) sourceMap.get("sourceCustomInfo");
+
+        ProductDetail productDetail = null;
+        if (commodityInfoMap != null) {
+            String commodityInfoTitle = (String) commodityInfoMap.get("commodityInfoTitle");
+            String commodityInfoDesc = (String) commodityInfoMap.get("commodityInfoDesc");
+            String pictureUrl = (String) commodityInfoMap.get("pictureUrl");
+            String commodityInfoUrl = (String) commodityInfoMap.get("commodityInfoUrl");
+            String note = (String) commodityInfoMap.get("note");
+            boolean show = false;
+            if (commodityInfoMap.containsKey("show"))
+                show = (boolean) commodityInfoMap.get("show");
+            boolean sendByUser = false;
+            if (commodityInfoMap.containsKey("sendByUser"))
+                sendByUser = (boolean) commodityInfoMap.get("sendByUser");
+
+            productDetail = new ProductDetail.Builder()
+                    .setTitle(commodityInfoTitle)
+                    .setDesc(commodityInfoDesc)
+                    .setPicture(pictureUrl)
+                    .setUrl(commodityInfoUrl)
+                    .setNote(note)
+                    .setShow(show ? 1 : 0)
+                    .setSendByUser(sendByUser)
+                    .build();
+        }
+
+        String sessionTitle = call.argument("sessionTitle");
+        long groupId = (int) call.argument("groupId");
+        long staffId = (int) call.argument("staffId");
+        long robotId = (int) call.argument("robotId");
+        boolean robotFirst = false;
+        if (call.hasArgument("robotFirst"))
+            robotFirst = (boolean) call.argument("robotFirst");
+        long faqTemplateId = (int) call.argument("faqTemplateId");
+        int vipLevel = (int) call.argument("vipLevel");
+        boolean showQuitQueue = false;
+        if (call.hasArgument("showQuitQueue"))
+            call.argument("showQuitQueue");
+        boolean showCloseSessionEntry = false;
+        if (call.hasArgument("showCloseSessionEntry"))
+            call.argument("showCloseSessionEntry");
+
+        // 启动聊天界面
+        ConsultSource source = new ConsultSource(sourceUrl, sourceTitle, sourceCustomInfo);
+        source.productDetail = productDetail;
+        source.groupId = groupId;
+        source.staffId = staffId;
+        source.robotId = robotId;
+        source.robotFirst = robotFirst;
+        source.faqGroupId = faqTemplateId;
+        source.vipLevel = vipLevel;
+        source.sessionLifeCycleOptions = new SessionLifeCycleOptions();
+        source.sessionLifeCycleOptions.setCanQuitQueue(showQuitQueue);
+        source.sessionLifeCycleOptions.setCanCloseSession(showCloseSessionEntry);
+        Unicorn.openServiceActivity(context, sessionTitle, source);
+    }
+
+    private void setCustomUIConfig(MethodCall call) {
+        // 会话窗口上方提示条中的文本字体颜色
+        String sessionTipTextColor = call.argument("sessionTipTextColor");
+        // 会话窗口上方提示条中的文本字体大小
+        int sessionTipTextFontSize = call.argument("sessionTipTextFontSize");
+        // 访客文本消息字体颜色
+        String customMessageTextColor = call.argument("customMessageTextColor");
+        // 客服文本消息字体颜色
+        String serviceMessageTextColor = call.argument("serviceMessageTextColor");
+        // 消息文本消息字体大小
+        int messageTextFontSize = call.argument("messageTextFontSize");
+        // 提示文本消息字体颜色
+        String tipMessageTextColor = call.argument("tipMessageTextColor");
+        // 提示文本消息字体大小
+        int tipMessageTextFontSize = call.argument("tipMessageTextFontSize");
+        // 输入框文本消息字体颜色
+        String inputTextColor = call.argument("inputTextColor");
+        // 输入框文本消息字体大小
+        int inputTextFontSize = call.argument("inputTextFontSize");
+        // 客服聊天窗口背景图片
+        String sessionBackgroundImage = call.argument("sessionBackgroundImage");
+        // 会话窗口上方提示条中的背景颜色
+        String sessionTipBackgroundColor = call.argument("sessionTipBackgroundColor");
+        // 访客头像
+        String customerHeadImage = call.argument("customerHeadImage");
+        // 客服头像
+        String serviceHeadImage = call.argument("serviceHeadImage");
+        // 消息竖直方向间距
+        float sessionMessageSpacing = (float) call.argument("sessionMessageSpacing");
+        // 是否显示头像
+        boolean showHeadImage = true;
+        if (call.hasArgument("showHeadImage"))
+            showHeadImage = call.argument("showHeadImage");
+        // 显示发送语音入口,设置为false,可以修改为隐藏
+        boolean showAudioEntry = true;
+        if (call.hasArgument("showAudioEntry"))
+            showAudioEntry = call.argument("showAudioEntry");
+        // 显示发送表情入口,设置为false,可以修改为隐藏
+        boolean showEmoticonEntry = true;
+        if (call.hasArgument("showEmoticonEntry")) call.argument("showEmoticonEntry");
+        // 进入聊天界面,是文本输入模式的话,会弹出键盘,设置为false,可以修改为不弹出
+        boolean autoShowKeyboard = true;
+        if (call.hasArgument("autoShowKeyboard ")) call.argument("autoShowKeyboard ");
+
+        UICustomization uiCustomization = ysfOptions.uiCustomization;
+        if (uiCustomization == null) {
+            uiCustomization = ysfOptions.uiCustomization = new UICustomization();
+        }
+        uiCustomization.topTipBarTextColor = QiYuUtils.parseColor(sessionTipTextColor);
+        uiCustomization.topTipBarTextSize = sessionTipTextFontSize;
+        uiCustomization.textMsgColorRight = QiYuUtils.parseColor(customMessageTextColor);
+        uiCustomization.textMsgColorLeft = QiYuUtils.parseColor(serviceMessageTextColor);
+        uiCustomization.textMsgSize = messageTextFontSize;
+        uiCustomization.tipsTextColor = QiYuUtils.parseColor(tipMessageTextColor);
+        uiCustomization.tipsTextSize = tipMessageTextFontSize;
+        uiCustomization.inputTextColor = QiYuUtils.parseColor(inputTextColor);
+        uiCustomization.inputTextSize = inputTextFontSize;
+        uiCustomization.msgBackgroundUri = QiYuUtils.getImageUri(this.context, sessionBackgroundImage);
+        uiCustomization.topTipBarBackgroundColor = QiYuUtils.parseColor(sessionTipBackgroundColor);
+        uiCustomization.rightAvatar = QiYuUtils.getImageUri(this.context, customerHeadImage);
+        uiCustomization.leftAvatar = QiYuUtils.getImageUri(this.context, serviceHeadImage);
+        uiCustomization.msgListViewDividerHeight = (int) sessionMessageSpacing;
+        uiCustomization.hideLeftAvatar = !showHeadImage;
+        uiCustomization.hideRightAvatar = !showHeadImage;
+        uiCustomization.hideAudio = !showAudioEntry;
+        uiCustomization.hideEmoji = !showEmoticonEntry;
+        uiCustomization.hideKeyboardOnEnterConsult = !autoShowKeyboard;
+//        Unicorn.updateOptions(ysfOptions);
+    }
+
+    private void getUnreadCount(MethodCall call, Result result) {
+        int count = Unicorn.getUnreadCount();
+        result.success(count);
+    }
+
+    private void setUserInfo(MethodCall call, final Result result) {
+        String userId = call.argument("userId");
+        String data = call.argument("data");
+        YSFUserInfo userInfo = new YSFUserInfo();
+        userInfo.userId = userId;
+        userInfo.data = data;
+        Unicorn.setUserInfo(userInfo, new RequestCallback<Void>() {
+            @Override
+            public void onSuccess(Void aVoid) {
+                Log.d("FLUTTER_QIYU", "SUCCESS");
+                result.success(true);
+            }
+
+            @Override
+            public void onFailed(int i) {
+                Log.d("FLUTTER_QIYU", "I");
+                result.error("failed" + i, "", null);
+            }
+
+            @Override
+            public void onException(Throwable throwable) {
+                result.error("error", throwable.toString(), throwable);
+            }
+        });
+    }
+
+    private void logout() {
+        Unicorn.setUserInfo(null);
+    }
+
+    private void cleanCache() {
+        Unicorn.clearCache();
+    }
+
+    private void setupChannel(BinaryMessenger messenger, Context context) {
+        this.context = context;
+        this.channel = new MethodChannel(messenger, CHANNEL_NAME);
+        this.channel.setMethodCallHandler(this);
+    }
+
+    private void teardownChannel() {
+        this.channel.setMethodCallHandler(null);
+        this.channel = null;
+    }
+
+//    public void onRequestPermission(String permission) {
+//        if (channel == null) return;
+//
+//        Map<String, Object> arguments = new HashMap<>();
+//        arguments.put("permission", permission);
+//        channel.invokeMethod("onRequestPermission", arguments);
+//    }
+}

+ 27 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/GlideGifImagerLoader.java

@@ -0,0 +1,27 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+import android.content.Context;
+import android.widget.ImageView;
+
+import com.bumptech.glide.Glide;
+import com.qiyukf.unicorn.api.UnicornGifImageLoader;
+
+import java.io.Serializable;
+
+public class GlideGifImagerLoader implements UnicornGifImageLoader, Serializable {
+
+    Context context;
+
+    public GlideGifImagerLoader(Context context) {
+        this.context = context.getApplicationContext();
+    }
+
+
+    @Override
+    public void loadGifImage(String url, ImageView imageView, String imgName) {
+        if (url == null || imgName == null) {
+            return;
+        }
+        Glide.with(context).load(url).into(imageView);
+    }
+}

+ 48 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/GlideImageLoader.java

@@ -0,0 +1,48 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.transition.Transition;
+import com.qiyukf.unicorn.api.ImageLoaderListener;
+import com.qiyukf.unicorn.api.UnicornImageLoader;
+
+public class GlideImageLoader implements UnicornImageLoader {
+    private Context context;
+
+    public GlideImageLoader(Context context) {
+        this.context = context.getApplicationContext();
+    }
+
+    //    @Nullable
+    @Override
+    public Bitmap loadImageSync(String uri, int width, int height) {
+        return null;
+    }
+
+    @Override
+    public void loadImage(String uri, int width, int height, final ImageLoaderListener listener) {
+        if (width <= 0 || height <= 0) {
+            width = height = Integer.MIN_VALUE;
+        }
+
+        Glide.with(context).asBitmap().load(uri).into(new SimpleTarget<Bitmap>(width, height) {
+            @Override
+            public void onResourceReady(Bitmap resource, Transition<? super Bitmap> glideAnimation) {
+                if (listener != null) {
+                    listener.onLoadComplete(resource);
+                }
+            }
+
+            @Override
+            public void onLoadFailed(Drawable errorDrawable) {
+                if (listener != null) {
+                    listener.onLoadFailed(null);
+                }
+            }
+        });
+    }
+}

+ 38 - 0
plugins/flutter_qiyu/android/src/main/java/org/leanflutter/plugins/flutter_qiyu/QiYuUtils.java

@@ -0,0 +1,38 @@
+package org.leanflutter.plugins.flutter_qiyu;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.text.TextUtils;
+
+public class QiYuUtils {
+    public static int parseColor(String colorString) {
+        int color = 0;
+        try {
+            color = Color.parseColor(colorString);
+        } catch (Exception e) {
+            // e.printStackTrace();
+        }
+        return color;
+    }
+
+    public static String getImageUri(Context context, String resName) {
+        if (TextUtils.isEmpty(resName)) {
+            return null;
+        }
+        if (resName.startsWith("./")) {
+            resName = resName.replace("./", "");
+        }
+        if (resName.contains(".")) {
+            resName = resName.substring(0, resName.indexOf("."));
+        }
+        resName = resName.replace("/", "_");
+        Resources res = context.getResources();
+        String pkgName = context.getPackageName();
+        int resId = res.getIdentifier(resName, "drawable", pkgName);
+        if (resId > 0) {
+            return "res:///" + resId;
+        }
+        return null;
+    }
+}

+ 5 - 0
plugins/flutter_qiyu/android/src/main/res/drawable/blue_button_background.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#4F6BE6" />
+    <corners android:radius="22dp" />
+</shape> 

+ 5 - 0
plugins/flutter_qiyu/android/src/main/res/drawable/dialog_background.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#FFFFFF" />
+    <corners android:radius="12dp" />
+</shape> 

+ 9 - 0
plugins/flutter_qiyu/android/src/main/res/drawable/ic_close.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#999999"
+        android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
+</vector> 

+ 50 - 0
plugins/flutter_qiyu/android/src/main/res/layout/permission_dialog.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="280dp"
+    android:layout_height="wrap_content"
+    android:background="@drawable/dialog_background"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="20dp"
+        android:paddingHorizontal="20dp">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="权限使用说明"
+            android:textColor="#333333"
+            android:textSize="16sp"
+            android:textStyle="bold" />
+
+        <ImageView
+            android:id="@+id/close_button"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_alignParentEnd="true"
+            android:src="@drawable/ic_close" />
+    </RelativeLayout>
+
+    <TextView
+        android:id="@+id/message"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="12dp"
+        android:layout_marginHorizontal="20dp"
+        android:textColor="#666666"
+        android:textSize="14sp" />
+
+    <Button
+        android:id="@+id/confirm_button"
+        android:layout_width="match_parent"
+        android:layout_height="44dp"
+        android:layout_margin="20dp"
+        android:background="@drawable/blue_button_background"
+        android:text="去授权"
+        android:textColor="#FFFFFF"
+        android:textSize="16sp"
+        android:stateListAnimator="@null"
+        android:elevation="0dp" />
+</LinearLayout> 

+ 17 - 0
plugins/flutter_qiyu/android/src/main/res/layout/permission_tip_view.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="#F5F5F5"
+    android:layout_marginTop="50dp"
+    android:paddingHorizontal="16dp"
+    android:paddingVertical="12dp"
+    android:orientation="vertical">
+
+    <TextView
+        android:id="@+id/tip_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textColor="#666666"
+        android:textSize="14sp" />
+</LinearLayout> 

+ 9 - 0
plugins/flutter_qiyu/ios/Classes/FlutterQiyuPlugin.h

@@ -0,0 +1,9 @@
+#import <Flutter/Flutter.h>
+
+@interface FlutterQiyuPlugin : NSObject <FlutterPlugin>
+@property(nonatomic, retain) FlutterMethodChannel *channel;
+@property(nonatomic, retain) UIViewController *viewController;
+
++ (void)registerWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar;
+
+@end

+ 349 - 0
plugins/flutter_qiyu/ios/Classes/FlutterQiyuPlugin.m

@@ -0,0 +1,349 @@
+#import <UIKit/UIKit.h>
+#import <NIMSDK/NIMSDK.h>
+#import <QYSDK/QYSDK.h>
+#import "FlutterQiyuPlugin.h"
+#import "UIBarButtonItem+blocks.h"
+
+@interface FlutterQiyuPlugin () <QYConversationManagerDelegate>
+@end
+
+@implementation FlutterQiyuPlugin
++ (void)registerWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {
+    // 高版本ios需要配置以下两行才能发送图片
+    [NIMSDK sharedSDK].serverSetting.nosUploadAddress = @"https://nosup-hz1.127.net";
+    [NIMSDK sharedSDK].serverSetting.nosUploadHost = @"nosup-hz1.127.net";
+    FlutterMethodChannel *channel = [FlutterMethodChannel
+            methodChannelWithName:@"flutter_qiyu"
+                  binaryMessenger:[registrar messenger]];
+    UIViewController *viewController = [UIApplication sharedApplication].delegate.window.rootViewController;
+    FlutterQiyuPlugin *instance = [[FlutterQiyuPlugin alloc] initWithViewController:viewController];
+    [registrar addMethodCallDelegate:instance channel:channel];
+    [registrar addApplicationDelegate:instance];
+
+    instance.channel = channel;
+
+    [[[QYSDK sharedSDK] conversationManager] setDelegate:instance];
+    [[QYSDK sharedSDK] registerPushMessageNotification:^(QYPushMessage *message) {
+        // TODO:
+    }];
+}
+
+- (instancetype)initWithViewController:(UIViewController *)viewController {
+    self = [super init];
+    if (self) {
+        self.viewController = viewController;
+    }
+    return self;
+}
+
+- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
+    NSDictionary *options = call.arguments;
+    if ([@"getPlatformVersion" isEqualToString:call.method]) {
+        result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
+    } else if ([@"registerApp" isEqualToString:call.method]) {
+        NSString *appKey = call.arguments[@"appKey"];
+        NSString *appName = call.arguments[@"appName"];
+        [self registerApp:appKey appName:appName];
+        result([NSNumber numberWithBool:YES]);
+    } else if ([@"openServiceWindow" isEqualToString:call.method]) {
+        [self openServiceWindow:options];
+        result([NSNumber numberWithBool:YES]);
+    } else if ([@"setCustomUIConfig" isEqualToString:call.method]) {
+        [self setCustomUIConfig:options];
+        result([NSNumber numberWithBool:YES]);
+    } else if ([@"getUnreadCount" isEqualToString:call.method]) {
+        NSInteger *unreadCount = [self getUnreadCount];
+        result([NSNumber numberWithInteger:unreadCount]);
+    } else if ([@"setUserInfo" isEqualToString:call.method]) {
+        [self setUserInfo:options];
+        result([NSNumber numberWithBool:YES]);
+    } else if ([@"logout" isEqualToString:call.method]) {
+        [self logout];
+        result([NSNumber numberWithBool:YES]);
+    } else if ([@"cleanCache" isEqualToString:call.method]) {
+        [self cleanCache];
+        result([NSNumber numberWithBool:YES]);
+    } else {
+        result(FlutterMethodNotImplemented);
+    }
+}
+
+- (void)registerApp:(NSString *)appKey
+            appName:(NSString *)appName {
+    [[QYSDK sharedSDK] registerAppId:appKey appName:appName];
+}
+
+- (void)openServiceWindow:(NSDictionary *)options {
+    NSDictionary *paramDict = options;
+    QYSessionViewController *sessionVC = [[QYSDK sharedSDK] sessionViewController];
+
+    QYSource *source = nil;
+    if ([paramDict objectForKey:@"source"]) {
+        NSDictionary *sourceDict = [paramDict objectForKey:@"source"];
+        if ([sourceDict objectForKey:@"sourceTitle"] || [sourceDict objectForKey:@"sourceUrl"]
+            || [sourceDict objectForKey:@"sourceCustomInfo"]) {
+            source = [[QYSource alloc] init];
+            if ([sourceDict objectForKey:@"sourceTitle"]) {
+                source.title = [sourceDict objectForKey:@"sourceTitle"];
+            }
+            if ([sourceDict objectForKey:@"sourceUrl"]) {
+                source.urlString = [sourceDict objectForKey:@"sourceUrl"];
+            }
+            if ([sourceDict objectForKey:@"sourceCustomInfo"]) {
+                source.customInfo = [sourceDict objectForKey:@"sourceCustomInfo"];
+            }
+        }
+    }
+    QYCommodityInfo *commodityInfo = nil;
+    if ([paramDict objectForKey:@"commodityInfo"]) {
+        NSDictionary *commodityInfoDict = [paramDict objectForKey:@"commodityInfo"];
+        if ([commodityInfoDict objectForKey:@"commodityInfoTitle"] ||
+            [commodityInfoDict objectForKey:@"commodityInfoDesc"]
+            || [commodityInfoDict objectForKey:@"pictureUrl"] ||
+            [commodityInfoDict objectForKey:@"commodityInfoUrl"]
+            || [commodityInfoDict objectForKey:@"note"] || [commodityInfoDict objectForKey:@"show"]
+            || [commodityInfoDict objectForKey:@"sendByUser"]) {
+            commodityInfo = [[QYCommodityInfo alloc] init];
+            if ([commodityInfoDict objectForKey:@"commodityInfoTitle"]) {
+                commodityInfo.title = [commodityInfoDict objectForKey:@"commodityInfoTitle"];
+            }
+            if ([commodityInfoDict objectForKey:@"commodityInfoDesc"]) {
+                commodityInfo.desc = [commodityInfoDict objectForKey:@"commodityInfoDesc"];
+            }
+            if ([commodityInfoDict objectForKey:@"pictureUrl"]) {
+                commodityInfo.pictureUrlString = [commodityInfoDict objectForKey:@"pictureUrl"];
+            }
+            if ([commodityInfoDict objectForKey:@"commodityInfoUrl"]) {
+                commodityInfo.urlString = [commodityInfoDict objectForKey:@"commodityInfoUrl"];
+            }
+            if ([commodityInfoDict objectForKey:@"note"]) {
+                commodityInfo.note = [commodityInfoDict objectForKey:@"note"];
+            }
+            if ([commodityInfoDict objectForKey:@"show"]) {
+                commodityInfo.show = [[commodityInfoDict objectForKey:@"show"] boolValue];
+            }
+            if ([commodityInfoDict objectForKey:@"sendByUser"]) {
+                commodityInfo.sendByUser = [[commodityInfoDict objectForKey:@"sendByUser"] boolValue];
+            }
+        }
+    }
+    if (source) {
+        sessionVC.source = source;
+    }
+    if (commodityInfo) {
+        sessionVC.commodityInfo = commodityInfo;
+    }
+    if ([paramDict objectForKey:@"sessionTitle"]) {
+        sessionVC.sessionTitle = [paramDict objectForKey:@"sessionTitle"];
+    }
+    if ([paramDict objectForKey:@"groupId"]) {
+        sessionVC.groupId = [[paramDict objectForKey:@"groupId"] intValue];
+    }
+    if ([paramDict objectForKey:@"staffId"]) {
+        sessionVC.staffId = [[paramDict objectForKey:@"staffId"] intValue];
+    }
+    if ([paramDict objectForKey:@"robotId"]) {
+        sessionVC.robotId = [[paramDict objectForKey:@"robotId"] intValue];
+    }
+    if ([paramDict objectForKey:@"vipLevel"]) {
+        sessionVC.vipLevel = [[paramDict objectForKey:@"vipLevel"] intValue];
+    }
+    if ([paramDict objectForKey:@"robotFirst"]) {
+        sessionVC.openRobotInShuntMode = [paramDict objectForKey:@"robotFirst"];
+    }
+    if ([paramDict objectForKey:@"faqTemplateId"]) {
+        sessionVC.commonQuestionTemplateId = [[paramDict objectForKey:@"faqTemplateId"] intValue];
+    }
+
+    [sessionVC.navigationItem setLeftBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain actionHandler:^{
+        [self.viewController dismissViewControllerAnimated:true completion:nil];
+    }]];
+
+    UINavigationController *rootNavigationController = [[UINavigationController alloc] initWithRootViewController:sessionVC];
+    [rootNavigationController setNavigationBarHidden:YES];
+    rootNavigationController.modalPresentationStyle = UIModalPresentationFullScreen;
+
+    [self.viewController presentViewController:rootNavigationController animated:YES completion:nil];
+}
+
+- (void)setCustomUIConfig:(NSDictionary *)options {
+    NSDictionary *paramDict = options;
+    if ([paramDict objectForKey:@"sessionTipTextColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].sessionTipTextColor = [self colorFromHexString:[paramDict objectForKey:@"sessionTipTextColor"]];
+    }
+    if ([paramDict objectForKey:@"sessionTipTextFontSize"]) {
+        [[QYSDK sharedSDK] customUIConfig].sessionTipTextFontSize = [[paramDict objectForKey:@"sessionTipTextFontSize"] floatValue];
+    }
+    if ([paramDict objectForKey:@"customMessageTextColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].customMessageTextColor = [self colorFromHexString:[paramDict objectForKey:@"customMessageTextColor"]];
+    }
+    if ([paramDict objectForKey:@"messageTextFontSize"]) {
+        [[QYSDK sharedSDK] customUIConfig].customMessageTextFontSize = [[paramDict objectForKey:@"messageTextFontSize"] floatValue];
+    }
+    if ([paramDict objectForKey:@"serviceMessageTextColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].serviceMessageTextColor = [self colorFromHexString:[paramDict objectForKey:@"serviceMessageTextColor"]];
+    }
+    if ([paramDict objectForKey:@"messageTextFontSize"]) {
+        [[QYSDK sharedSDK] customUIConfig].serviceMessageTextFontSize = [[paramDict objectForKey:@"messageTextFontSize"] floatValue];
+    }
+    if ([paramDict objectForKey:@"tipMessageTextColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].tipMessageTextColor = [self colorFromHexString:[paramDict objectForKey:@"tipMessageTextColor"]];
+    }
+    if ([paramDict objectForKey:@"tipMessageTextFontSize"]) {
+        [[QYSDK sharedSDK] customUIConfig].tipMessageTextFontSize = [[paramDict objectForKey:@"tipMessageTextFontSize"] floatValue];
+    }
+    if ([paramDict objectForKey:@"inputTextColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].inputTextColor = [self colorFromHexString:[paramDict objectForKey:@"inputTextColor"]];
+    }
+    if ([paramDict objectForKey:@"inputTextFontSize"]) {
+        [[QYSDK sharedSDK] customUIConfig].inputTextFontSize = [[paramDict objectForKey:@"inputTextFontSize"] floatValue];
+    }
+    NSString *imageName = nil;
+    if ([paramDict objectForKey:@"sessionBackgroundImage"]) {
+        imageName = [paramDict objectForKey:@"sessionBackgroundImage"];
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [[QYSDK sharedSDK] customUIConfig].sessionBackground = [[UIImageView alloc] initWithImage:[self getResourceImage:imageName]];
+        });
+    }
+    if ([paramDict objectForKey:@"sessionTipBackgroundColor"]) {
+        [[QYSDK sharedSDK] customUIConfig].sessionTipBackgroundColor = [self colorFromHexString:[paramDict objectForKey:@"sessionTipBackgroundColor"]];
+    }
+    if ([paramDict objectForKey:@"customerHeadImage"]) {
+        imageName = [paramDict objectForKey:@"customerHeadImage"];
+        [[QYSDK sharedSDK] customUIConfig].customerHeadImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"serviceHeadImage"]) {
+        imageName = [paramDict objectForKey:@"serviceHeadImage"];
+        [[QYSDK sharedSDK] customUIConfig].serviceHeadImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"customerMessageBubbleNormalImage"]) {
+        imageName = [paramDict objectForKey:@"customerMessageBubbleNormalImage"];
+        [[QYSDK sharedSDK] customUIConfig].customerMessageBubbleNormalImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"serviceMessageBubbleNormalImage"]) {
+        imageName = [paramDict objectForKey:@"serviceMessageBubbleNormalImage"];
+        [[QYSDK sharedSDK] customUIConfig].serviceMessageBubbleNormalImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"customerMessageBubblePressedImage"]) {
+        imageName = [paramDict objectForKey:@"customerMessageBubblePressedImage"];
+        [[QYSDK sharedSDK] customUIConfig].customerMessageBubblePressedImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"serviceMessageBubblePressedImage"]) {
+        imageName = [paramDict objectForKey:@"serviceMessageBubblePressedImage"];
+        [[QYSDK sharedSDK] customUIConfig].serviceMessageBubblePressedImage = [self getResourceImage:imageName];
+    }
+    if ([paramDict objectForKey:@"sessionMessageSpacing"]) {
+        [[QYSDK sharedSDK] customUIConfig].sessionMessageSpacing = [[paramDict objectForKey:@"sessionMessageSpacing"] floatValue];
+    }
+    if ([paramDict objectForKey:@"showHeadImage"]) {
+        [[QYSDK sharedSDK] customUIConfig].showHeadImage = [paramDict objectForKey:@"showHeadImage"];
+    }
+//    if ([paramDict objectForKey:@"naviBarColor"]) {
+//        self.naviBarColor = [self colorFromString:[paramDict objectForKey:@"naviBarColor"]];
+//    }
+//    if ([paramDict objectForKey:@"naviBarStyleDark"]) {
+//        [[QYSDK sharedSDK] customUIConfig].rightBarButtonItemColorBlackOrWhite = [[paramDict objectForKey:@"naviBarStyleDark"] boolValue];
+//    }
+    if ([paramDict objectForKey:@"showAudioEntry"]) {
+        [[QYSDK sharedSDK] customUIConfig].showAudioEntry = [[paramDict objectForKey:@"showAudioEntry"] boolValue];
+    }
+    if ([paramDict objectForKey:@"showEmoticonEntry"]) {
+        [[QYSDK sharedSDK] customUIConfig].showEmoticonEntry = [[paramDict objectForKey:@"showEmoticonEntry"] boolValue];
+    }
+    if ([paramDict objectForKey:@"autoShowKeyboard"]) {
+        [[QYSDK sharedSDK] customUIConfig].autoShowKeyboard = [[paramDict objectForKey:@"autoShowKeyboard"] boolValue];
+    }
+    if ([paramDict objectForKey:@"bottomMargin"]) {
+        [[QYSDK sharedSDK] customUIConfig].bottomMargin = [[paramDict objectForKey:@"bottomMargin"] floatValue];
+    }
+//    if ([paramDict objectForKey:@"showCloseSessionEntry"]) {
+//        [[QYSDK sharedSDK] customUIConfig].showCloseSessionEntry = [RCTConvert BOOL:[paramDict objectForKey:@"showCloseSessionEntry"]];
+//    }
+}
+
+- (NSInteger *)getUnreadCount {
+    NSInteger count = [[[QYSDK sharedSDK] conversationManager] allUnreadCount];
+    return count;
+}
+
+- (void)setUserInfo:(NSDictionary *)options {
+    NSDictionary *paramDict = options;
+
+    QYUserInfo *userInfo = nil;
+    if ([paramDict objectForKey:@"userId"] || [paramDict objectForKey:@"data"]) {
+        userInfo = [[QYUserInfo alloc] init];
+        if ([paramDict objectForKey:@"userId"]) {
+            userInfo.userId = [paramDict objectForKey:@"userId"];
+        }
+        if ([paramDict objectForKey:@"data"]) {
+            userInfo.data = [paramDict objectForKey:@"data"];
+        }
+    }
+    if (userInfo) {
+        [[QYSDK sharedSDK] setUserInfo:userInfo];
+    }
+}
+
+- (void)logout {
+    [[QYSDK sharedSDK] logout:nil];
+}
+
+- (void)cleanCache {
+    [[QYSDK sharedSDK] cleanResourceCacheWithBlock:nil];
+}
+
+- (UIColor *)colorFromHexString:(NSString *)hexString {
+    NSString *cleanString = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""];
+    if ([cleanString length] == 3) {
+        cleanString = [NSString stringWithFormat:@"%@%@%@%@%@%@",
+                                                 [cleanString substringWithRange:NSMakeRange(0,
+                                                                                             1)], [cleanString substringWithRange:NSMakeRange(
+                        0, 1)],
+                                                 [cleanString substringWithRange:NSMakeRange(1,
+                                                                                             1)], [cleanString substringWithRange:NSMakeRange(
+                        1, 1)],
+                                                 [cleanString substringWithRange:NSMakeRange(2,
+                                                                                             1)], [cleanString substringWithRange:NSMakeRange(
+                        2, 1)]];
+    }
+    if ([cleanString length] == 6) {
+        cleanString = [cleanString stringByAppendingString:@"ff"];
+    }
+
+    unsigned int baseValue;
+    [[NSScanner scannerWithString:cleanString] scanHexInt:&baseValue];
+
+    float red = ((baseValue >> 24) & 0xFF) / 255.0f;
+    float green = ((baseValue >> 16) & 0xFF) / 255.0f;
+    float blue = ((baseValue >> 8) & 0xFF) / 255.0f;
+    float alpha = ((baseValue >> 0) & 0xFF) / 255.0f;
+
+    return [UIColor colorWithRed:red green:green blue:blue alpha:alpha];
+}
+
+- (UIImage *)getResourceImage:(NSString *)imageFilePath {
+    NSString *localImagePath = [imageFilePath substringFromIndex:1];
+    NSString *bundlePath = [NSBundle mainBundle].bundlePath;
+    bundlePath = [[bundlePath stringByAppendingPathComponent:@"assets"] stringByAppendingPathComponent:localImagePath];
+
+    UIImage *image = [[UIImage imageWithContentsOfFile:bundlePath] resizableImageWithCapInsets:UIEdgeInsetsMake(
+            15, 15, 30,
+            30)                                                    resizingMode:UIImageResizingModeStretch];
+    if (image) {
+        return image;
+    }
+
+    return nil;
+}
+
+- (void)onUnreadCountChanged:(NSInteger)count {
+    [_channel invokeMethod:@"onUnreadCountChange" arguments:@{
+            @"unreadCount": [NSNumber numberWithInteger:count]}];
+}
+
+- (void)                             application:(UIApplication *)app
+didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
+    [[QYSDK sharedSDK] updateApnsToken:deviceToken];
+}
+
+@end

+ 38 - 0
plugins/flutter_qiyu/ios/Classes/UIBarButtonItem+blocks.h

@@ -0,0 +1,38 @@
+//
+//  UIBarButtonItem+blocks.h
+//
+//  Created by Julian Weinert on 04.08.14.
+//  Copyright (c) 2014 Julian Weinert Softwareentwicklung. All rights reserved.
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 2 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#import <UIKit/UIKit.h>
+
+typedef void (^UIBarButtonItemActionHandler)();
+
+@interface UIBarButtonItem (blocks)
+
+- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler;
+
+- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler NS_AVAILABLE_IOS
+
+(5_0);
+
+- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler;
+
+- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem actionHandler:(UIBarButtonItemActionHandler)actionHandler;
+
+- (void)setActionHandler:(UIBarButtonItemActionHandler)actionHandler;
+
+@end

+ 62 - 0
plugins/flutter_qiyu/ios/Classes/UIBarButtonItem+blocks.m

@@ -0,0 +1,62 @@
+//
+//  UIBarButtonItem+blocks.m
+//
+//  Created by Julian Weinert on 04.08.14.
+//  Copyright (c) 2014 Julian Weinert Softwareentwicklung. All rights reserved.
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 2 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+#import "UIBarButtonItem+blocks.h"
+#import <objc/runtime.h>
+
+@implementation UIBarButtonItem (blocks)
+
+- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler {
+    if (self = [self initWithImage:image style:style target:self action:@selector(performActionHandler)]) {
+        [self setActionHandler:actionHandler];
+    }
+    return self;
+}
+
+- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler {
+    if (self = [self initWithImage:image landscapeImagePhone:landscapeImagePhone style:style target:self action:@selector(performActionHandler)]) {
+        [self setActionHandler:actionHandler];
+    }
+    return self;
+}
+
+- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style actionHandler:(UIBarButtonItemActionHandler)actionHandler {
+    if (self = [self initWithTitle:title style:style target:self action:@selector(performActionHandler)]) {
+        [self setActionHandler:actionHandler];
+    }
+    return self;
+}
+
+- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem actionHandler:(UIBarButtonItemActionHandler)actionHandler {
+    if (self = [self initWithBarButtonSystemItem:systemItem target:self action:@selector(performActionHandler)]) {
+        [self setActionHandler:actionHandler];
+    }
+    return self;
+}
+
+- (void)setActionHandler:(UIBarButtonItemActionHandler)actionHandler {
+    objc_setAssociatedObject(self, "actionHandler", actionHandler, OBJC_ASSOCIATION_COPY_NONATOMIC);
+}
+
+- (void)performActionHandler {
+    UIBarButtonItemActionHandler actionHandler = objc_getAssociatedObject(self, "actionHandler");
+    actionHandler();
+}
+
+@end

+ 26 - 0
plugins/flutter_qiyu/ios/flutter_qiyu.podspec

@@ -0,0 +1,26 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint flutter_qiyu.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'flutter_qiyu'
+  s.version          = '0.0.1'
+  s.summary          = 'A new Flutter plugin project.'
+  s.description      = <<-DESC
+A new Flutter plugin project.
+                       DESC
+  s.homepage         = 'http://example.com'
+  s.license          = { :file => '../LICENSE' }
+  s.author           = { 'Your Company' => 'email@example.com' }
+  s.source           = { :path => '.' }
+  s.source_files = 'Classes/**/*'
+  s.public_header_files = 'Classes/**/*.h'
+  s.dependency 'Flutter'
+  s.dependency 'QY_iOS_SDK', '~> 9.1.0'
+  s.platform = :ios, '11.0'
+
+  # Flutter.framework does not contain a i386 slice.
+  # s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+  s.static_framework = true
+end

+ 5 - 0
plugins/flutter_qiyu/lib/flutter_qiyu.dart

@@ -0,0 +1,5 @@
+export './qiyu.dart';
+export './qy_commodity_info.dart';
+export './qy_service_window_params.dart';
+export './qy_source.dart';
+export './qy_user_info_params.dart';

+ 98 - 0
plugins/flutter_qiyu/lib/qiyu.dart

@@ -0,0 +1,98 @@
+import 'dart:async';
+
+import 'package:flutter/services.dart';
+
+import './qy_service_window_params.dart';
+import './qy_user_info_params.dart';
+
+typedef UnreadCountChangeListener(int unreadCount);
+typedef PermissionListener(String permission);
+
+class QiYuMethodCallHandler {
+  QiYuMethodCallHandler();
+
+  List<UnreadCountChangeListener> _unreadCountChangeListeners = [];
+
+  void register(dynamic listener) {
+    if (listener is UnreadCountChangeListener) {
+      _unreadCountChangeListeners.add(listener);
+    }
+  }
+
+  void unregister(dynamic listener) {
+    if (listener is UnreadCountChangeListener) {
+      _unreadCountChangeListeners.removeWhere((v) => v == listener);
+    }
+  }
+
+  Future<dynamic> handler(MethodCall call) {
+    switch (call.method) {
+      case 'onUnreadCountChange':
+        for (var unreadCountChangeListener in _unreadCountChangeListeners) {
+          int unreadCount = call.arguments['unreadCount'];
+          unreadCountChangeListener(unreadCount);
+        }
+        break;
+      default:
+        throw new UnsupportedError("Unrecognized Method");
+    }
+    return null!;
+  }
+}
+
+class QiYu {
+  static const MethodChannel _channel = const MethodChannel('flutter_qiyu');
+
+  static QiYuMethodCallHandler _methodCallHandler = QiYuMethodCallHandler();
+
+  static void registerListener(dynamic listener) {
+    _methodCallHandler.register(listener);
+  }
+
+  static void unregisterListener(dynamic listener) {
+    _methodCallHandler.unregister(listener);
+  }
+
+  static void onUnreadCountChange(UnreadCountChangeListener listener) {
+    _methodCallHandler.register(listener);
+  }
+
+  static Future<bool> registerApp({String? appKey, String? appName}) async {
+    _channel.setMethodCallHandler(_methodCallHandler.handler);
+
+    return await _channel.invokeMethod('registerApp', {
+      'appKey': appKey,
+      'appName': appName,
+    });
+  }
+
+  static Future<bool> openServiceWindow(QYServiceWindowParams params) async {
+    return await _channel.invokeMethod('openServiceWindow', params.toJson());
+  }
+
+  static Future<bool> setCustomUIConfig(Map params) async {
+    return await _channel.invokeMethod('setCustomUIConfig', params);
+  }
+
+  static Future<int> getUnreadCount() async {
+    return await _channel.invokeMethod('getUnreadCount', {});
+  }
+
+  static Future<bool> setUserInfo(QYUserInfoParams params) async {
+    return await _channel.invokeMethod('setUserInfo', params.toJson());
+  }
+
+  static Future<bool> logout() async {
+    return await _channel.invokeMethod('logout', {});
+  }
+
+  static Future<bool> cleanCache() async {
+    return await _channel.invokeMethod('cleanCache', {});
+  }
+
+  // 清理资源
+  static Future<bool> cleanUp() async {
+    final result = await _channel.invokeMethod('cleanUp');
+    return result; // 返回清理结果
+  }
+}

+ 47 - 0
plugins/flutter_qiyu/lib/qy_commodity_info.dart

@@ -0,0 +1,47 @@
+class QYCommodityInfo {
+  String? commodityInfoTitle;
+  String? commodityInfoDesc;
+  String? pictureUrl;
+  String? commodityInfoUrl;
+  String? note;
+  bool? show;
+  bool? sendByUser;
+
+  QYCommodityInfo({
+    this.commodityInfoTitle,
+    this.commodityInfoDesc,
+    this.pictureUrl,
+    this.commodityInfoUrl,
+    this.note,
+    this.show,
+    this.sendByUser,
+  });
+
+  factory QYCommodityInfo.fromJson(Map<String, dynamic> json) {
+    return QYCommodityInfo(
+      commodityInfoTitle: json['commodityInfoTitle'],
+      commodityInfoDesc: json['commodityInfoDesc'],
+      pictureUrl: json['pictureUrl'],
+      commodityInfoUrl: json['commodityInfoUrl'],
+      note: json['note'],
+      show: json['show'],
+      sendByUser: json['sendByUser'],
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> json = new Map();
+
+    if (commodityInfoTitle != null)
+      json.putIfAbsent('commodityInfoTitle', () => commodityInfoTitle);
+    if (commodityInfoDesc != null)
+      json.putIfAbsent('commodityInfoDesc', () => commodityInfoDesc);
+    if (pictureUrl != null) json.putIfAbsent('pictureUrl', () => pictureUrl);
+    if (commodityInfoUrl != null)
+      json.putIfAbsent('commodityInfoUrl', () => commodityInfoUrl);
+    if (note != null) json.putIfAbsent('note', () => note);
+    if (show != null) json.putIfAbsent('show', () => show);
+    if (sendByUser != null) json.putIfAbsent('sendByUser', () => sendByUser);
+    return json;
+  }
+}

+ 70 - 0
plugins/flutter_qiyu/lib/qy_service_window_params.dart

@@ -0,0 +1,70 @@
+import './qy_commodity_info.dart';
+import './qy_source.dart';
+
+class QYServiceWindowParams {
+  QYSource? source;
+  QYCommodityInfo? commodityInfo;
+
+  String? sessionTitle;
+  int groupId;
+  int staffId;
+  int? robotId;
+  bool robotFirst;
+  int faqTemplateId;
+  int vipLevel;
+  bool showQuitQueue;
+  bool showCloseSessionEntry;
+
+  QYServiceWindowParams({
+    this.source,
+    this.commodityInfo,
+    this.sessionTitle,
+    this.groupId = 0,
+    this.staffId = 0,
+    this.robotId,
+    this.robotFirst = false,
+    this.faqTemplateId = 0,
+    this.vipLevel = 0,
+    this.showQuitQueue = true,
+    this.showCloseSessionEntry = true,
+  });
+
+  factory QYServiceWindowParams.fromJson(Map<String, dynamic> json) {
+    return QYServiceWindowParams(
+      source: QYSource.fromJson(json['source']),
+      commodityInfo: json.containsKey('commodityInfo')
+          ? QYCommodityInfo.fromJson(json['commodityInfo'])
+          : null,
+      sessionTitle: json['sessionTitle'],
+      groupId: json['groupId'],
+      staffId: json['staffId'],
+      robotId: json['robotId'],
+      robotFirst: json['robotFirst'],
+      faqTemplateId: json['faqTemplateId'],
+      vipLevel: json['vipLevel'],
+      showQuitQueue: json['showQuitQueue'],
+      showCloseSessionEntry: json['showCloseSessionEntry'],
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    Map<String, dynamic> json = new Map();
+    if (source != null) json.putIfAbsent('source', () => source!.toJson());
+    if (commodityInfo != null)
+      json.putIfAbsent('commodityInfo', () => commodityInfo!.toJson());
+    if (sessionTitle != null)
+      json.putIfAbsent('sessionTitle', () => sessionTitle);
+    if (groupId != null) json.putIfAbsent('groupId', () => groupId);
+    if (staffId != null) json.putIfAbsent('staffId', () => staffId);
+    if (robotId != null) json.putIfAbsent('robotId', () => robotId);
+    if (robotFirst != null) json.putIfAbsent('robotFirst', () => robotFirst);
+    if (faqTemplateId != null)
+      json.putIfAbsent('faqTemplateId', () => faqTemplateId);
+    if (vipLevel != null) json.putIfAbsent('vipLevel', () => vipLevel);
+    if (showQuitQueue != null)
+      json.putIfAbsent('showQuitQueue', () => showQuitQueue);
+    if (showCloseSessionEntry != null)
+      json.putIfAbsent('showCloseSessionEntry', () => showCloseSessionEntry);
+    return json;
+  }
+}

+ 27 - 0
plugins/flutter_qiyu/lib/qy_source.dart

@@ -0,0 +1,27 @@
+class QYSource {
+  String? sourceTitle;
+  String? sourceUrl;
+  String? sourceCustomInfo;
+
+  QYSource({
+    this.sourceTitle,
+    this.sourceUrl,
+    this.sourceCustomInfo,
+  });
+
+  factory QYSource.fromJson(Map<String, dynamic> json) {
+    return QYSource(
+      sourceTitle: json['sourceTitle'],
+      sourceUrl: json['sourceUrl'],
+      sourceCustomInfo: json['sourceCustomInfo'],
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'sourceTitle': sourceTitle,
+      'sourceUrl': sourceUrl,
+      'sourceCustomInfo': sourceCustomInfo,
+    };
+  }
+}

+ 23 - 0
plugins/flutter_qiyu/lib/qy_user_info_params.dart

@@ -0,0 +1,23 @@
+class QYUserInfoParams {
+  String? userId;
+  String? data;
+
+  QYUserInfoParams({
+    this.userId,
+    this.data,
+  });
+
+  factory QYUserInfoParams.fromJson(Map<String, dynamic> json) {
+    return QYUserInfoParams(
+      userId: json['userId'],
+      data: json['data'],
+    );
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'userId': userId,
+      'data': data,
+    };
+  }
+}

+ 205 - 0
plugins/flutter_qiyu/pubspec.lock

@@ -0,0 +1,205 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  async:
+    dependency: transitive
+    description:
+      name: async
+      sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.11.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.1.1"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.19.0"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.1"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.3"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  leak_tracker:
+    dependency: transitive
+    description:
+      name: leak_tracker
+      sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
+      url: "https://pub.dev"
+    source: hosted
+    version: "10.0.7"
+  leak_tracker_flutter_testing:
+    dependency: transitive
+    description:
+      name: leak_tracker_flutter_testing
+      sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.8"
+  leak_tracker_testing:
+    dependency: transitive
+    description:
+      name: leak_tracker_testing
+      sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.1"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.1"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.12.16+1"
+  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: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.15.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.9.0"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.10.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.12.0"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.2"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.3.0"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.2.1"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+      url: "https://pub.dev"
+    source: hosted
+    version: "0.7.3"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.1.4"
+  vm_service:
+    dependency: transitive
+    description:
+      name: vm_service
+      sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
+      url: "https://pub.dev"
+    source: hosted
+    version: "14.3.0"
+sdks:
+  dart: ">=3.4.0 <4.0.0"
+  flutter: ">=3.18.0-18.0.pre.54"

+ 26 - 0
plugins/flutter_qiyu/pubspec.yaml

@@ -0,0 +1,26 @@
+name: flutter_qiyu
+description: 适用于 Flutter 的七鱼客服插件
+version: 0.1.2
+homepage: https://github.com/leanflutter/flutter_qiyu
+
+environment:
+  sdk: ">=2.12.0 <4.0.0"
+  flutter: ">=2.5.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^2.0.0
+
+flutter:
+  plugin:
+    platforms:
+      android:
+        package: org.leanflutter.plugins.flutter_qiyu
+        pluginClass: FlutterQiyuPlugin
+      ios:
+        pluginClass: FlutterQiyuPlugin

+ 7 - 0
pubspec.lock

@@ -367,6 +367,13 @@ packages:
       relative: true
     source: path
     version: "0.0.1"
+  flutter_qiyu:
+    dependency: "direct main"
+    description:
+      path: "plugins/flutter_qiyu"
+      relative: true
+    source: path
+    version: "0.1.2"
   flutter_screenutil:
     dependency: "direct main"
     description:

+ 4 - 0
pubspec.yaml

@@ -106,6 +106,10 @@ dependencies:
   #拨号
   url_launcher: 6.3.1
 
+  #七鱼
+  flutter_qiyu:
+    path: plugins/flutter_qiyu
+
   ######################地图########################
   flutter_map:
     path: plugins/map