Преглед изворни кода

[feat]新增定制人设,提交部分漏掉代码

云天逵 пре 1 година
родитељ
комит
4231a19f4c
71 измењених фајлова са 3740 додато и 137 уклоњено
  1. BIN
      assets/images/bg_character_custom_detail.webp
  2. BIN
      assets/images/icon_arrow_right.webp
  3. BIN
      assets/images/icon_character_custom_detail_edit.webp
  4. BIN
      assets/images/icon_character_custom_detail_lock.webp
  5. BIN
      assets/images/icon_character_custom_detail_male.webp
  6. BIN
      assets/images/icon_character_custom_detail_switch.webp
  7. BIN
      assets/images/icon_character_custom_plus.webp
  8. BIN
      assets/images/icon_custom_dialog_close.webp
  9. 23 1
      assets/string/base/string.xml
  10. 17 0
      lib/data/api/atmob_api.dart
  11. 76 0
      lib/data/api/atmob_api.g.dart
  12. 38 0
      lib/data/api/request/character_custom_generate_request.dart
  13. 88 0
      lib/data/api/request/character_custom_generate_request.g.dart
  14. 17 0
      lib/data/api/response/character_custom_config_response.dart
  15. 22 0
      lib/data/api/response/character_custom_config_response.g.dart
  16. 18 0
      lib/data/api/response/character_custom_generate_response.dart
  17. 19 0
      lib/data/api/response/character_custom_generate_response.g.dart
  18. 2 2
      lib/data/bean/character_info.dart
  19. 1 1
      lib/data/bean/character_info.g.dart
  20. 93 0
      lib/data/bean/custom_config_info.dart
  21. 74 0
      lib/data/bean/custom_config_info.g.dart
  22. 20 0
      lib/data/repository/characters_repository.dart
  23. 14 2
      lib/data/repository/config_repository.dart
  24. 37 3
      lib/di/get_it.config.dart
  25. 82 0
      lib/dialog/character_add_dialog.dart
  26. 106 0
      lib/dialog/content/character_add_tab_controller.dart
  27. 141 0
      lib/dialog/content/character_add_tab_view.dart
  28. 163 0
      lib/dialog/content/character_tab_group_content_controller.dart
  29. 228 0
      lib/dialog/content/character_tab_group_content_view.dart
  30. 144 0
      lib/dialog/custom_label_dialog.dart
  31. 201 7
      lib/module/character_custom/character_custom_controller.dart
  32. 339 103
      lib/module/character_custom/character_custom_page.dart
  33. 40 0
      lib/module/character_custom/detail/character_custom_detail_controller.dart
  34. 358 0
      lib/module/character_custom/detail/character_custom_detail_page.dart
  35. 17 8
      lib/module/keyboard_manage/keyboard_manage_controller.dart
  36. 2 2
      lib/module/keyboard_manage/keyboard_manage_page.dart
  37. 44 0
      lib/resource/assets.gen.dart
  38. 28 2
      lib/resource/string.gen.dart
  39. 7 3
      lib/router/app_pages.dart
  40. 48 0
      lib/utils/styles.dart
  41. 33 0
      plugins/keyboard_android/.gitignore
  42. 33 0
      plugins/keyboard_android/.metadata
  43. 3 0
      plugins/keyboard_android/CHANGELOG.md
  44. 1 0
      plugins/keyboard_android/LICENSE
  45. 15 0
      plugins/keyboard_android/README.md
  46. 4 0
      plugins/keyboard_android/analysis_options.yaml
  47. 9 0
      plugins/keyboard_android/android/.gitignore
  48. 68 0
      plugins/keyboard_android/android/build.gradle
  49. 1 0
      plugins/keyboard_android/android/settings.gradle
  50. 32 0
      plugins/keyboard_android/android/src/main/AndroidManifest.xml
  51. 191 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/CustomKeyboardService.kt
  52. 147 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/FloatingButtonService.kt
  53. 61 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/InputMethodPickerActivity.kt
  54. 130 0
      plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/KeyboardAndroidPlugin.kt
  55. 14 0
      plugins/keyboard_android/android/src/main/res/layout/floating_button_layout.xml
  56. 57 0
      plugins/keyboard_android/android/src/main/res/layout/keyboard_layout.xml
  57. 4 0
      plugins/keyboard_android/android/src/main/res/xml/keyboard_method.xml
  58. 27 0
      plugins/keyboard_android/android/src/test/kotlin/com/atmob/keyboard_android/KeyboardAndroidPluginTest.kt
  59. 38 0
      plugins/keyboard_android/ios/.gitignore
  60. 0 0
      plugins/keyboard_android/ios/Assets/.gitkeep
  61. 19 0
      plugins/keyboard_android/ios/Classes/KeyboardAndroidPlugin.swift
  62. 14 0
      plugins/keyboard_android/ios/Resources/PrivacyInfo.xcprivacy
  63. 29 0
      plugins/keyboard_android/ios/keyboard_android.podspec
  64. 36 0
      plugins/keyboard_android/lib/keyboard_android.dart
  65. 70 0
      plugins/keyboard_android/lib/keyboard_android_method_channel.dart
  66. 66 0
      plugins/keyboard_android/lib/keyboard_android_platform_interface.dart
  67. 72 0
      plugins/keyboard_android/pubspec.yaml
  68. 27 0
      plugins/keyboard_android/test/keyboard_android_method_channel_test.dart
  69. 29 0
      plugins/keyboard_android/test/keyboard_android_test.dart
  70. 2 2
      pubspec.lock
  71. 1 1
      pubspec.yaml

BIN
assets/images/bg_character_custom_detail.webp


BIN
assets/images/icon_arrow_right.webp


BIN
assets/images/icon_character_custom_detail_edit.webp


BIN
assets/images/icon_character_custom_detail_lock.webp


BIN
assets/images/icon_character_custom_detail_male.webp


BIN
assets/images/icon_character_custom_detail_switch.webp


BIN
assets/images/icon_character_custom_plus.webp


BIN
assets/images/icon_custom_dialog_close.webp


+ 23 - 1
assets/string/base/string.xml

@@ -94,6 +94,28 @@
     <string name="keyboard_save_failed">保存失败</string>
     <string name="add_character">添加人设</string>
     <string name="custom_character">定制人设</string>
-    <string name="character_custom_steps_desc">专属于你独一无二的人设</string>
+
+
+    <!--   定制人设页面 -->
+
+    <string name="character_custom_steps_desc">量身打造 自定义人设更贴脸</string>
+    <string name="character_custom_hobbies_title">用爱好开启对话,发现更多可能性</string>
+    <string name="character_custom_character_title">最想用哪三个特质打动TA</string>
+    <string name="character_custom_name_title">给人设取一个名字</string>
+    <string name="character_custom_customizable">自定义</string>
+    <string name="character_custom_next_step">下一步</string>
+    <string name="character_custom_name_hint">输入你的名字</string>
+    <string name="character_custom_history">定制历史</string>
+
+        <!--    自定义标签-->
+    <string name="custom_label">自定义标签</string>
+    <string name="custom_label_hobbies_hint">输入你的爱好</string>
+    <string name="custom_label_character_hint">输入你的特质</string>
+    <string name="custom_label_name_hint">输入你的名字</string>
+    <string name="custom_label_save">保存</string>
+
+
+    <!--    生成定制人设页面-->
+    <string name="unlock_exclusive_character">解锁专属人设</string>
 
 </resources>

+ 17 - 0
lib/data/api/atmob_api.dart

@@ -1,6 +1,7 @@
 import 'package:dio/dio.dart';
 import 'package:keyboard/base/base_response.dart';
 import 'package:keyboard/data/api/request/character_add_request.dart';
+import 'package:keyboard/data/api/request/character_custom_generate_request.dart';
 import 'package:keyboard/data/api/request/character_page_request.dart';
 import 'package:keyboard/data/api/request/character_unlock_request.dart';
 import 'package:keyboard/data/api/request/complaint_submit_request.dart';
@@ -13,6 +14,8 @@ import 'package:keyboard/data/api/request/login_request.dart';
 import 'package:keyboard/data/api/request/send_code_request.dart';
 import 'package:keyboard/data/api/request/user_info_setting_request.dart';
 import 'package:keyboard/data/api/response/character_add_response.dart';
+import 'package:keyboard/data/api/response/character_custom_config_response.dart';
+import 'package:keyboard/data/api/response/character_custom_generate_response.dart';
 import 'package:keyboard/data/api/response/character_group_response.dart';
 import 'package:keyboard/data/api/response/character_page_response.dart';
 import 'package:keyboard/data/api/response/character_unlock_response.dart';
@@ -92,6 +95,18 @@ abstract class AtmobApi {
     @Body() CharacterAddRequest request,
   );
 
+  // 获取定制人设配置
+  @POST("/project/keyboard/v1/character/custom/config")
+  Future<BaseResponse<CharacterCustomConfigResponse>> getCharacterCustomConfig(
+    @Body() AppBaseRequest request,
+  );
+
+  // 生成定制人设
+  @POST("/project/keyboard/v1/character/custom/generate")
+  Future<BaseResponse<CharacterCustomGenerateResponse>> generateCharacterCustom(
+    @Body() CharacterCustomGenerateRequest request,
+  );
+
   // 获取键盘人设列表
   @POST("/project/keyboard/v1/character/list")
   Future<BaseResponse<KeyboardCharacterListResponse>> getKeyboardCharacterList(
@@ -117,4 +132,6 @@ abstract class AtmobApi {
   //获取配置信息
   @POST("/project/keyboard/v1/confs")
   Future<BaseResponse<ConfigResponse>> confs(@Body() ConfigRequest request);
+
+
 }

+ 76 - 0
lib/data/api/atmob_api.g.dart

@@ -379,6 +379,82 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
+  Future<BaseResponse<CharacterCustomConfigResponse>> getCharacterCustomConfig(
+    AppBaseRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options =
+        _setStreamType<BaseResponse<CharacterCustomConfigResponse>>(
+          Options(method: 'POST', headers: _headers, extra: _extra)
+              .compose(
+                _dio.options,
+                '/project/keyboard/v1/character/custom/config',
+                queryParameters: queryParameters,
+                data: _data,
+              )
+              .copyWith(
+                baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
+              ),
+        );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<CharacterCustomConfigResponse> _value;
+    try {
+      _value = BaseResponse<CharacterCustomConfigResponse>.fromJson(
+        _result.data!,
+        (json) => CharacterCustomConfigResponse.fromJson(
+          json as Map<String, dynamic>,
+        ),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
+  Future<BaseResponse<CharacterCustomGenerateResponse>> generateCharacterCustom(
+    CharacterCustomGenerateRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options =
+        _setStreamType<BaseResponse<CharacterCustomGenerateResponse>>(
+          Options(method: 'POST', headers: _headers, extra: _extra)
+              .compose(
+                _dio.options,
+                '/project/keyboard/v1/character/custom/generate',
+                queryParameters: queryParameters,
+                data: _data,
+              )
+              .copyWith(
+                baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
+              ),
+        );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<CharacterCustomGenerateResponse> _value;
+    try {
+      _value = BaseResponse<CharacterCustomGenerateResponse>.fromJson(
+        _result.data!,
+        (json) => CharacterCustomGenerateResponse.fromJson(
+          json as Map<String, dynamic>,
+        ),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
   Future<BaseResponse<KeyboardCharacterListResponse>> getKeyboardCharacterList(
     KeyboardCharacterListRequest request,
   ) async {

+ 38 - 0
lib/data/api/request/character_custom_generate_request.dart

@@ -0,0 +1,38 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+part 'character_custom_generate_request.g.dart';
+
+@JsonSerializable()
+class CharacterCustomGenerateRequest extends AppBaseRequest {
+  @JsonKey(name: "name")
+  String? name;
+
+  @JsonKey(name: "birthday")
+  String? birthday;
+
+  @JsonKey(name: "imageUrl")
+  String? imageUrl;
+
+  @JsonKey(name: "gender")
+  int? gender;
+
+  @JsonKey(name: "hobbies")
+  List<String>? hobbies;
+
+  @JsonKey(name: "characters")
+  List<String>? characters;
+
+  CharacterCustomGenerateRequest(
+      {this.name,
+    this.birthday,
+    this.imageUrl,
+    this.gender,
+    this.hobbies,
+    this.characters,}
+  );
+
+  @override
+  Map<String, dynamic> toJson() => _$CharacterCustomGenerateRequestToJson(this);
+}

+ 88 - 0
lib/data/api/request/character_custom_generate_request.g.dart

@@ -0,0 +1,88 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'character_custom_generate_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CharacterCustomGenerateRequest _$CharacterCustomGenerateRequestFromJson(
+  Map<String, dynamic> json,
+) =>
+    CharacterCustomGenerateRequest(
+        name: json['name'] as String?,
+        birthday: json['birthday'] as String?,
+        imageUrl: json['imageUrl'] as String?,
+        gender: (json['gender'] as num?)?.toInt(),
+        hobbies:
+            (json['hobbies'] as List<dynamic>?)
+                ?.map((e) => e as String)
+                .toList(),
+        characters:
+            (json['characters'] as List<dynamic>?)
+                ?.map((e) => e as String)
+                .toList(),
+      )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$CharacterCustomGenerateRequestToJson(
+  CharacterCustomGenerateRequest instance,
+) => <String, dynamic>{
+  'appPlatform': instance.appPlatform,
+  'os': instance.os,
+  'osVersion': instance.osVersion,
+  'packageName': instance.packageName,
+  'appVersionName': instance.appVersionName,
+  'appVersionCode': instance.appVersionCode,
+  'channelName': instance.channelName,
+  'appId': instance.appId,
+  'tgPlatform': instance.tgPlatform,
+  'oaid': instance.oaid,
+  'aaid': instance.aaid,
+  'androidId': instance.androidId,
+  'imei': instance.imei,
+  'simImei0': instance.simImei0,
+  'simImei1': instance.simImei1,
+  'mac': instance.mac,
+  'idfa': instance.idfa,
+  'idfv': instance.idfv,
+  'machineId': instance.machineId,
+  'brand': instance.brand,
+  'model': instance.model,
+  'wifiName': instance.wifiName,
+  'region': instance.region,
+  'locLng': instance.locLng,
+  'locLat': instance.locLat,
+  'authToken': instance.authToken,
+  'name': instance.name,
+  'birthday': instance.birthday,
+  'imageUrl': instance.imageUrl,
+  'gender': instance.gender,
+  'hobbies': instance.hobbies,
+  'characters': instance.characters,
+};

+ 17 - 0
lib/data/api/response/character_custom_config_response.dart

@@ -0,0 +1,17 @@
+import 'package:json_annotation/json_annotation.dart';
+import 'package:keyboard/data/bean/custom_config_info.dart';
+
+import '../../bean/character_info.dart';
+
+part 'character_custom_config_response.g.dart';
+
+@JsonSerializable()
+class CharacterCustomConfigResponse {
+  @JsonKey(name: "customConfig")
+  CustomConfigInfo? characterInfo;
+
+  CharacterCustomConfigResponse({this.characterInfo});
+
+  factory CharacterCustomConfigResponse.fromJson(Map<String, dynamic> json) =>
+      _$CharacterCustomConfigResponseFromJson(json);
+}

+ 22 - 0
lib/data/api/response/character_custom_config_response.g.dart

@@ -0,0 +1,22 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'character_custom_config_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CharacterCustomConfigResponse _$CharacterCustomConfigResponseFromJson(
+  Map<String, dynamic> json,
+) => CharacterCustomConfigResponse(
+  characterInfo:
+      json['customConfig'] == null
+          ? null
+          : CustomConfigInfo.fromJson(
+            json['customConfig'] as Map<String, dynamic>,
+          ),
+);
+
+Map<String, dynamic> _$CharacterCustomConfigResponseToJson(
+  CharacterCustomConfigResponse instance,
+) => <String, dynamic>{'customConfig': instance.characterInfo};

+ 18 - 0
lib/data/api/response/character_custom_generate_response.dart

@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../bean/character_info.dart';
+
+
+part 'character_custom_generate_response.g.dart';
+
+@JsonSerializable()
+class CharacterCustomGenerateResponse {
+
+  @JsonKey(name: "characterInfo")
+  late final CharacterInfo characterInfo;
+
+  CharacterCustomGenerateResponse({ required this.characterInfo});
+
+  factory CharacterCustomGenerateResponse.fromJson(Map<String, dynamic> json) =>
+      _$CharacterCustomGenerateResponseFromJson(json);
+}

+ 19 - 0
lib/data/api/response/character_custom_generate_response.g.dart

@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'character_custom_generate_response.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CharacterCustomGenerateResponse _$CharacterCustomGenerateResponseFromJson(
+  Map<String, dynamic> json,
+) => CharacterCustomGenerateResponse(
+  characterInfo: CharacterInfo.fromJson(
+    json['characterInfo'] as Map<String, dynamic>,
+  ),
+);
+
+Map<String, dynamic> _$CharacterCustomGenerateResponseToJson(
+  CharacterCustomGenerateResponse instance,
+) => <String, dynamic>{'characterInfo': instance.characterInfo};

+ 2 - 2
lib/data/bean/character_info.dart

@@ -6,7 +6,7 @@ part 'character_info.g.dart';
 class CharacterInfo {
   //人设id
   @JsonKey(name: 'id')
-  String id;
+  String? id;
 
   //人设名称
   @JsonKey(name: 'name')
@@ -51,7 +51,7 @@ class CharacterInfo {
   int? gender;
 
   CharacterInfo({
-    required this.id,
+    this.id,
     this.name,
     this.imageUrl,
     this.description,

+ 1 - 1
lib/data/bean/character_info.g.dart

@@ -8,7 +8,7 @@ part of 'character_info.dart';
 
 CharacterInfo _$CharacterInfoFromJson(Map<String, dynamic> json) =>
     CharacterInfo(
-      id: json['id'] as String,
+      id: json['id'] as String?,
       name: json['name'] as String?,
       imageUrl: json['imageUrl'] as String?,
       description: json['description'] as String?,

+ 93 - 0
lib/data/bean/custom_config_info.dart

@@ -0,0 +1,93 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'custom_config_info.g.dart';
+
+@JsonSerializable()
+class CustomConfigInfo {
+  //爱好列表
+  @JsonKey(name: 'hobbies')
+  List<Hobbies>? hobbies;
+
+  //是否可自定义爱好标签
+  @JsonKey(name: 'customHobby')
+  bool? customHobby;
+
+
+  // 爱好最小选择数量
+  @JsonKey(name: 'minHobbyNum')
+  int? minHobbyNum;
+
+  // 爱好最大选择数量
+  @JsonKey(name: 'maxHobbyNum')
+  int? maxHobbyNum;
+
+  //爱好最大字符数
+  @JsonKey(name: 'maxHobbyWords')
+  int? maxHobbyWords;
+
+  // 男生头像列表
+  @JsonKey(name: 'boyAvatars')
+  List<String>? boyAvatars;
+
+  // 女生头像列表
+  @JsonKey(name: 'girlAvatars')
+  List<String>? girlAvatars;
+
+  // 性格列表
+  @JsonKey(name: 'characters')
+  List<CharactersList>? characters;
+
+  // 是否可自定义性格标签
+  @JsonKey(name: 'customCharacter')
+  bool? customCharacter;
+
+  // 性格最小选择数量
+  @JsonKey(name: 'minCharacterNum')
+  int? minCharacterNum;
+
+  // 性格最大选择数量
+  @JsonKey(name: 'maxCharacterNum')
+  int? maxCharacterNum;
+
+  // 性格最大字符数
+  @JsonKey(name: 'maxCharacterWords')
+  int? maxCharacterWords;
+
+
+  CustomConfigInfo({this.hobbies, this.customHobby, this.minHobbyNum,
+      this.maxHobbyNum, this.maxHobbyWords, this.boyAvatars, this.girlAvatars,
+      this.characters, this.customCharacter, this.minCharacterNum,
+      this.maxCharacterNum, this.maxCharacterWords});
+
+  factory CustomConfigInfo.fromJson(Map<String, dynamic> json) =>
+      _$CustomConfigInfoFromJson(json);
+
+  Map<String, dynamic> toJson() => _$CustomConfigInfoToJson(this);
+}
+
+@JsonSerializable()
+class Hobbies {
+  int? id;
+  String? name;
+  String? emoji;
+
+  Hobbies({this.id, this.name,this.emoji});
+
+  factory Hobbies.fromJson(Map<String, dynamic> json) =>
+      _$HobbiesFromJson(json);
+
+  Map<String, dynamic> toJson() => _$HobbiesToJson(this);
+}
+@JsonSerializable()
+class CharactersList {
+  int? id;
+  String? name;
+  String? emoji;
+
+  CharactersList({this.id, this.name, this.emoji});
+
+  factory CharactersList.fromJson(Map<String, dynamic> json) =>
+      _$CharactersListFromJson(json);
+
+  Map<String, dynamic> toJson() => _$CharactersListToJson(this);
+}

+ 74 - 0
lib/data/bean/custom_config_info.g.dart

@@ -0,0 +1,74 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'custom_config_info.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CustomConfigInfo _$CustomConfigInfoFromJson(
+  Map<String, dynamic> json,
+) => CustomConfigInfo(
+  hobbies:
+      (json['hobbies'] as List<dynamic>?)
+          ?.map((e) => Hobbies.fromJson(e as Map<String, dynamic>))
+          .toList(),
+  customHobby: json['customHobby'] as bool?,
+  minHobbyNum: (json['minHobbyNum'] as num?)?.toInt(),
+  maxHobbyNum: (json['maxHobbyNum'] as num?)?.toInt(),
+  maxHobbyWords: (json['maxHobbyWords'] as num?)?.toInt(),
+  boyAvatars:
+      (json['boyAvatars'] as List<dynamic>?)?.map((e) => e as String).toList(),
+  girlAvatars:
+      (json['girlAvatars'] as List<dynamic>?)?.map((e) => e as String).toList(),
+  characters:
+      (json['characters'] as List<dynamic>?)
+          ?.map((e) => CharactersList.fromJson(e as Map<String, dynamic>))
+          .toList(),
+  customCharacter: json['customCharacter'] as bool?,
+  minCharacterNum: (json['minCharacterNum'] as num?)?.toInt(),
+  maxCharacterNum: (json['maxCharacterNum'] as num?)?.toInt(),
+  maxCharacterWords: (json['maxCharacterWords'] as num?)?.toInt(),
+);
+
+Map<String, dynamic> _$CustomConfigInfoToJson(CustomConfigInfo instance) =>
+    <String, dynamic>{
+      'hobbies': instance.hobbies,
+      'customHobby': instance.customHobby,
+      'minHobbyNum': instance.minHobbyNum,
+      'maxHobbyNum': instance.maxHobbyNum,
+      'maxHobbyWords': instance.maxHobbyWords,
+      'boyAvatars': instance.boyAvatars,
+      'girlAvatars': instance.girlAvatars,
+      'characters': instance.characters,
+      'customCharacter': instance.customCharacter,
+      'minCharacterNum': instance.minCharacterNum,
+      'maxCharacterNum': instance.maxCharacterNum,
+      'maxCharacterWords': instance.maxCharacterWords,
+    };
+
+Hobbies _$HobbiesFromJson(Map<String, dynamic> json) => Hobbies(
+  id: (json['id'] as num?)?.toInt(),
+  name: json['name'] as String?,
+  emoji: json['emoji'] as String?,
+);
+
+Map<String, dynamic> _$HobbiesToJson(Hobbies instance) => <String, dynamic>{
+  'id': instance.id,
+  'name': instance.name,
+  'emoji': instance.emoji,
+};
+
+CharactersList _$CharactersListFromJson(Map<String, dynamic> json) =>
+    CharactersList(
+      id: (json['id'] as num?)?.toInt(),
+      name: json['name'] as String?,
+      emoji: json['emoji'] as String?,
+    );
+
+Map<String, dynamic> _$CharactersListToJson(CharactersList instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'name': instance.name,
+      'emoji': instance.emoji,
+    };

+ 20 - 0
lib/data/repository/characters_repository.dart

@@ -10,8 +10,10 @@ import '../../utils/atmob_log.dart';
 import '../../utils/http_handler.dart';
 import '../api/atmob_api.dart';
 import '../api/request/character_add_request.dart';
+import '../api/request/character_custom_generate_request.dart';
 import '../api/request/character_page_request.dart';
 import '../api/request/character_unlock_request.dart';
+import '../api/response/character_custom_generate_response.dart';
 import '../api/response/character_group_response.dart';
 import '../api/response/character_page_response.dart';
 import '../bean/character_group_info.dart';
@@ -32,6 +34,7 @@ class CharactersRepository {
     getCharactersGroup();
   }
 
+
   // 获取主题
   Future<CharacterGroupResponse> getCharactersGroup() async {
     return atmobApi
@@ -89,6 +92,23 @@ class CharactersRepository {
         .then(HttpHandler.handle(false));
   }
 
+
+  ///生成自定义人设
+  Future<CharacterCustomGenerateResponse> generateCharacterCustom({
+    String?name,
+    String?birthday,
+    String?imageUrl,
+    int?gender,
+    List<String>?hobbies,
+    List<String>?characters,
+  }
+      ) {
+    return atmobApi
+        .generateCharacterCustom(CharacterCustomGenerateRequest(name: name,birthday: birthday,imageUrl: imageUrl,gender: gender,hobbies: hobbies, characters: characters))
+        .then(HttpHandler.handle(true));
+  }
+
+
   static CharactersRepository getInstance() =>
       getIt.get<CharactersRepository>();
 }

+ 14 - 2
lib/data/repository/config_repository.dart

@@ -1,6 +1,7 @@
 import 'package:injectable/injectable.dart';
+import 'package:keyboard/data/api/response/character_custom_config_response.dart';
 
-import '../../di/get_it.dart';
+import '../../base/app_base_request.dart';
 import '../../utils/async_util.dart';
 import '../../utils/atmob_log.dart';
 import '../../utils/http_handler.dart';
@@ -11,11 +12,14 @@ import '../api/response/config_response.dart';
 @lazySingleton
 class ConfigRepository {
   final tag = "ConfigRepository";
+
   final AtmobApi atmobApi;
 
   ConfigRepository(this.atmobApi) {
     AtmobLog.d(tag, '$tag....init');
     refreshConfig();
+
+
   }
 
   // 更新配置的值
@@ -29,7 +33,6 @@ class ConfigRepository {
       if (list == null || list.isEmpty) {
         return;
       }
-      AtmobLog.d(tag, 'refreshConfig....list: $list');
     });
   }
 
@@ -39,4 +42,13 @@ class ConfigRepository {
         .confs(ConfigRequest(confCodes: ['intimacy', 'default_avatar']))
         .then(HttpHandler.handle(true));
   }
+
+  /// 获取定制人设配置
+  Future<CharacterCustomConfigResponse> getCharacterCustomConfig() {
+    return atmobApi
+        .getCharacterCustomConfig(AppBaseRequest())
+        .then(HttpHandler.handle(true));
+  }
+
+
 }

+ 37 - 3
lib/di/get_it.config.dart

@@ -15,17 +15,23 @@ import 'package:injectable/injectable.dart' as _i526;
 
 import '../data/api/atmob_api.dart' as _i243;
 import '../data/api/atmob_stream_api.dart' as _i329;
+import '../data/bean/character_group_info.dart' as _i96;
+import '../data/bean/keyboard_info.dart' as _i497;
 import '../data/repository/account_repository.dart' as _i83;
 import '../data/repository/characters_repository.dart' as _i421;
 import '../data/repository/chat_repository.dart' as _i425;
 import '../data/repository/config_repository.dart' as _i50;
 import '../data/repository/keyboard_repository.dart' as _i274;
+import '../dialog/content/character_add_tab_controller.dart' as _i991;
+import '../dialog/content/character_tab_group_content_controller.dart' as _i293;
 import '../module/about/about_controller.dart' as _i256;
 import '../module/browser/browser_controller.dart' as _i923;
 import '../module/character/character_controller.dart' as _i888;
 import '../module/character/content/character_group_content_controller.dart'
     as _i970;
 import '../module/character_custom/character_custom_controller.dart' as _i15;
+import '../module/character_custom/detail/character_custom_detail_controller.dart'
+    as _i79;
 import '../module/feedback/feedback_controller.dart' as _i876;
 import '../module/keyboard_manage/keyboard_manage_controller.dart' as _i922;
 import '../module/login/login_controller.dart' as _i1008;
@@ -44,9 +50,6 @@ extension GetItInjectableX on _i174.GetIt {
     final networkModule = _$NetworkModule();
     gh.factory<_i256.AboutController>(() => _i256.AboutController());
     gh.factory<_i923.BrowserController>(() => _i923.BrowserController());
-    gh.factory<_i15.CharacterCustomController>(
-      () => _i15.CharacterCustomController(),
-    );
     gh.factory<_i1008.LoginController>(() => _i1008.LoginController());
     gh.factory<_i731.MainController>(() => _i731.MainController());
     gh.singleton<_i361.Dio>(
@@ -85,6 +88,18 @@ extension GetItInjectableX on _i174.GetIt {
     gh.lazySingleton<_i274.KeyboardRepository>(
       () => _i274.KeyboardRepository(gh<_i243.AtmobApi>()),
     );
+    gh.factoryParam<
+      _i293.CharacterTabGroupContentController,
+      _i96.CharacterGroupInfo,
+      _i497.KeyboardInfo
+    >(
+      (currentCharacterGroupInfo, currentKeyboardInfo) =>
+          _i293.CharacterTabGroupContentController(
+            gh<_i421.CharactersRepository>(),
+            currentCharacterGroupInfo: currentCharacterGroupInfo,
+            currentKeyboardInfo: currentKeyboardInfo,
+          ),
+    );
     gh.factory<_i922.KeyboardManageController>(
       () => _i922.KeyboardManageController(gh<_i274.KeyboardRepository>()),
     );
@@ -94,6 +109,20 @@ extension GetItInjectableX on _i174.GetIt {
     gh.factory<_i732.MineController>(
       () => _i732.MineController(gh<_i83.AccountRepository>()),
     );
+    gh.factoryParam<
+      _i991.CharacterAddTabController,
+      _i497.KeyboardInfo,
+      dynamic
+    >(
+      (currentKeyboardInfo, _) => _i991.CharacterAddTabController(
+        gh<_i421.CharactersRepository>(),
+        gh<_i274.KeyboardRepository>(),
+        currentKeyboardInfo: currentKeyboardInfo,
+      ),
+    );
+    gh.factory<_i15.CharacterCustomController>(
+      () => _i15.CharacterCustomController(gh<_i50.ConfigRepository>()),
+    );
     gh.factory<_i888.CharacterController>(
       () => _i888.CharacterController(
         gh<_i421.CharactersRepository>(),
@@ -106,6 +135,11 @@ extension GetItInjectableX on _i174.GetIt {
         gh<_i421.CharactersRepository>(),
       ),
     );
+    gh.factory<_i79.CharacterCustomDetailController>(
+      () => _i79.CharacterCustomDetailController(
+        gh<_i421.CharactersRepository>(),
+      ),
+    );
     return this;
   }
 }

+ 82 - 0
lib/dialog/character_add_dialog.dart

@@ -0,0 +1,82 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:keyboard/data/bean/keyboard_info.dart';
+
+import '../resource/assets.gen.dart';
+import '../resource/colors.gen.dart';
+import 'content/character_add_tab_view.dart';
+
+class CharacterAddDialog {
+  static const String tag = 'CharacterAddDialog';
+
+  static void show({
+    required KeyboardInfo currentKeyboardInfo,
+    required VoidCallback clickCallback,
+  }) {
+    SmartDialog.show(
+      tag: tag,
+      backType: SmartBackType.block,
+      clickMaskDismiss: false,
+      maskColor: ColorName.black70,
+      builder: (_) {
+        return SizedBox(
+          width: double.infinity,
+          height: double.infinity,
+          child: Column(
+            mainAxisAlignment: MainAxisAlignment.end,
+            children: [
+              Container(
+                padding: EdgeInsets.only(
+                  bottom: 28.h,
+                  left: 16.w,
+                  right: 16.w,
+                  top: 22.h,
+                ),
+                alignment: Alignment.topCenter,
+                width: double.infinity,
+                height: 712.h,
+                decoration: BoxDecoration(
+                  color: const Color(0xFFF6F5FA),
+                  borderRadius: BorderRadius.circular(20.r),
+                ),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  mainAxisAlignment: MainAxisAlignment.start,
+                  children: [
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Assets.images.iconCharacterMarket.image(
+                          width: 73.w,
+                          height: 25.h,
+                        ),
+                        GestureDetector(
+                          onTap: () {
+                            clickCallback();
+                            SmartDialog.dismiss();
+                          },
+                          child: Assets.images.iconDialogCloseBlack.image(
+                            width: 24.w,
+                            height: 24.w,
+                          ),
+                        ),
+                      ],
+                    ),
+                    SizedBox(height: 20.h),
+                    Expanded(
+                      child: CharacterAddTabView(
+                        currentKeyboardInfo: currentKeyboardInfo,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}

+ 106 - 0
lib/dialog/content/character_add_tab_controller.dart

@@ -0,0 +1,106 @@
+import 'package:flutter/material.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:get/get.dart';
+import '../../data/bean/character_group_info.dart';
+import '../../data/bean/keyboard_info.dart';
+import '../../data/repository/characters_repository.dart';
+import '../../data/repository/keyboard_repository.dart';
+import '../../utils/atmob_log.dart';
+
+@injectable
+class CharacterAddTabController extends BaseController
+    with GetSingleTickerProviderStateMixin {
+  final String tag = "CharacterAddTabController";
+
+  final CharactersRepository charactersRepository;
+  final KeyboardRepository keyboardRepository;
+
+  // 键盘列表
+  RxList<KeyboardInfo> get keyboardInfoList =>
+      keyboardRepository.keyboardInfoList;
+
+  late Rx<TabController> tabController;
+  late PageController pageController;
+
+  Rx<CharacterGroupInfo> currentCharacterGroupInfo = CharacterGroupInfo().obs;
+
+  KeyboardInfo currentKeyboardInfo;
+
+  RxList<CharacterGroupInfo> get characterGroupList =>
+      charactersRepository.characterGroupList;
+
+  @factoryMethod
+  CharacterAddTabController(
+    this.charactersRepository,
+    this.keyboardRepository, {
+    @factoryParam required this.currentKeyboardInfo,
+  });
+
+  RxInt currentTabBarIndex = 0.obs;
+
+  @override
+  void onInit() {
+    super.onInit();
+    _dataLoad();
+  }
+
+  void _dataLoad() async {
+    pageController = PageController();
+    tabController =
+        TabController(
+          length: characterGroupList.length,
+          vsync: this,
+          initialIndex: 0,
+        ).obs;
+
+    ever(charactersRepository.characterGroupList, (value) {
+      AtmobLog.d(tag, "characterGroupList changed");
+      if (value.isNotEmpty) {
+        tabController.value.dispose();
+        tabController.value = TabController(
+          length: characterGroupList.length,
+          vsync: this,
+          initialIndex: 0,
+        );
+        currentCharacterGroupInfo.value = characterGroupList.first;
+        AtmobLog.d(
+          tag,
+          "currentCharacterGroupInfo.value: ${characterGroupList.first.id}",
+        );
+      }
+    });
+
+    ever(keyboardRepository.keyboardInfoList, (value) {
+      AtmobLog.d(tag, "keyboardInfoList1 changed");
+      if (value.isNotEmpty) {
+        currentKeyboardInfo = keyboardInfoList.first;
+        print("currentKeyboardInfo.value: $currentKeyboardInfo");
+      }
+    });
+  }
+
+  void onTabChanged(int index) {
+    if (index >= characterGroupList.length) {
+      return;
+    }
+    currentTabBarIndex.value = index;
+    pageController.animateToPage(
+      index,
+      duration: const Duration(milliseconds: 300),
+      curve: Curves.easeInToLinear,
+    );
+  }
+
+  void onPageChanged(int index) {
+    if (index >= characterGroupList.length) {
+      return;
+    }
+
+    currentCharacterGroupInfo.value = characterGroupList[index];
+    tabController.value.animateTo(
+      index,
+      duration: const Duration(milliseconds: 300),
+    );
+  }
+}

+ 141 - 0
lib/dialog/content/character_add_tab_view.dart

@@ -0,0 +1,141 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/base/base_view.dart';
+import 'package:keyboard/dialog/content/character_add_tab_controller.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+
+import '../../data/bean/keyboard_info.dart';
+import '../../data/repository/characters_repository.dart';
+import '../../data/repository/keyboard_repository.dart';
+import '../../di/get_it.dart';
+import '../../resource/assets.gen.dart';
+import 'character_tab_group_content_view.dart';
+
+// 人设列表弹窗使用的Tab
+class CharacterAddTabView extends BaseView<CharacterAddTabController> {
+
+  final KeyboardInfo currentKeyboardInfo;
+
+  @override
+  String? get tag => "CharacterAddTabController ${currentKeyboardInfo.id}";
+  const CharacterAddTabView({super.key, required this.currentKeyboardInfo}) ;
+
+  @override
+  backgroundColor() => Colors.transparent;
+
+  @override
+  Widget buildBody(BuildContext context) {
+    Get.delete<CharacterAddTabController>(tag: tag);
+    Get.put(CharacterAddTabController(
+        getIt.get<CharactersRepository>(),
+        getIt.get<KeyboardRepository>(),
+        currentKeyboardInfo: currentKeyboardInfo),
+        tag: tag);
+
+    return Column(children: [
+      _tabBar(),
+
+      Expanded(child: _pages())
+    ]);
+  }
+
+
+  /// **TabBar**
+  Widget _tabBar() {
+    return Obx(() {
+      if (controller.characterGroupList.isEmpty) {
+        return const SizedBox.shrink();
+      }
+      return TabBar(
+        controller: controller.tabController.value,
+        dividerHeight: 0,
+        tabAlignment: TabAlignment.start,
+        isScrollable: true,
+        padding: EdgeInsets.symmetric(horizontal: 12.w),
+        labelPadding: EdgeInsets.symmetric(horizontal: 4.w),
+        indicator: const BoxDecoration(),
+
+        onTap: (index) => controller.onTabChanged(index),
+        tabs: List.generate(controller.characterGroupList.length, (index) {
+          var e = controller.characterGroupList[index];
+          bool isSelected = index == controller.currentTabBarIndex.value;
+          return Column(
+            children: [
+              Container(
+                width: 80.w,
+                height: isSelected ? 38.h : 32.h,
+                padding:
+                    isSelected ? EdgeInsets.only(bottom: 4.h) : EdgeInsets.zero,
+                decoration:
+                    isSelected
+                        ? BoxDecoration(
+                          borderRadius: BorderRadius.circular(36.r),
+                          image: DecorationImage(
+                            image:
+                                Assets.images.iconCharacterGroupSelected
+                                    .provider(),
+                            fit: BoxFit.fill,
+                          ),
+                        )
+                        : BoxDecoration(
+                          color: Colors.white.withAlpha(204),
+                          borderRadius: BorderRadius.circular(36.r),
+                        ),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    if (e.iconUrl != null)
+                      CachedNetworkImage(
+                        imageUrl: e.iconUrl!,
+                        width: 20.r,
+                        height: 20.r,
+                      ),
+
+                    Text(
+                      e.name ?? "",
+                      style: TextStyle(
+                        color:
+                            isSelected
+                                ? Colors.black
+                                : Colors.black.withAlpha(104),
+                        fontSize: 14.sp,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              !isSelected ? SizedBox(height: 4.h) : SizedBox(),
+            ],
+          );
+        }),
+      );
+    });
+  }
+
+  Widget _pages() {
+    return Obx(() {
+      if (controller.characterGroupList.isEmpty) {
+        return const Center(child: CircularProgressIndicator());
+      }
+      return Padding(
+        padding: const EdgeInsets.only(top: 8.0),
+        child: PageView(
+          controller: controller.pageController,
+          onPageChanged: (index) {
+            controller.onPageChanged(index);
+          },
+          children:
+              controller.characterGroupList.map((group) {
+
+                return CharacterTabGroupContentView(
+                  currentKeyboardInfo: controller.currentKeyboardInfo,
+                  characterGroupInfo: group,
+                );
+              }).toList(),
+        ),
+      );
+    });
+  }
+}

+ 163 - 0
lib/dialog/content/character_tab_group_content_controller.dart

@@ -0,0 +1,163 @@
+import 'package:easy_refresh/easy_refresh.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/data/repository/characters_repository.dart';
+import 'package:keyboard/dialog/character_details_dialog.dart';
+import 'package:keyboard/utils/atmob_log.dart';
+import 'package:keyboard/utils/http_handler.dart';
+import 'package:keyboard/utils/toast_util.dart';
+
+import '../../../data/bean/character_group_info.dart';
+import '../../../data/bean/character_info.dart';
+import '../../../data/bean/keyboard_info.dart';
+import '../../../utils/error_handler.dart';
+
+@injectable
+class CharacterTabGroupContentController extends BaseController {
+   final KeyboardInfo currentKeyboardInfo ;
+
+   final CharacterGroupInfo currentCharacterGroupInfo;
+
+  final CharactersRepository charactersRepository;
+
+  @factoryMethod
+  CharacterTabGroupContentController(this.charactersRepository,{
+    @factoryParam required this.currentCharacterGroupInfo,
+    @factoryParam required this.currentKeyboardInfo,
+  });
+  RxList<CharacterInfo> characterList = <CharacterInfo>[].obs;
+
+  RxInt currentListCount = 0.obs;
+
+  RxInt currentPage = 1.obs;
+  late EasyRefreshController refreshController;
+
+  @override
+  void onInit() async {
+    super.onInit();
+    refreshController = EasyRefreshController(
+      controlFinishLoad: true,
+      controlFinishRefresh: true,
+    );
+    // 等待页面渲染完成后再加载数据
+    WidgetsBinding.instance.addPostFrameCallback((_) {
+      refreshData();
+    });
+
+  }
+
+  @override
+  onReady() {
+    super.onReady();
+
+    AtmobLog.d("CharacterTabGroupContentController", "onReady");
+  }
+
+  // 下拉刷新
+  Future<void> refreshData() async {
+    currentPage.value = 1;
+
+    await getCurrentCharacterListInfo(isRefresh: true);
+    refreshController.finishRefresh();
+    refreshController.resetFooter(); // 允许加载更多
+  }
+
+  // 上拉加载更多
+  Future<void> loadMoreData() async {
+    if (characterList.length >= currentListCount.value) {
+      refreshController.finishLoad(IndicatorResult.noMore);
+      return;
+    }
+    currentPage.value++;
+    await getCurrentCharacterListInfo(isRefresh: false);
+    refreshController.finishLoad(IndicatorResult.success);
+  }
+
+  // 获取角色列表
+  Future<void> getCurrentCharacterListInfo({bool isRefresh = false}) async {
+    var response = await charactersRepository.getCharactersPage(
+      groupId: currentCharacterGroupInfo.id.toString(),
+      page: currentPage.value,
+      keyboardId: currentKeyboardInfo.id.toString(),
+    );
+    if (isRefresh) {
+
+      characterList.value = response.characterInfos;
+    } else {
+      characterList.addAll(response.characterInfos);
+    }
+    currentListCount.value = response.count;
+  }
+
+  @override
+  void onClose() {
+    refreshController.dispose();
+    super.onClose();
+  }
+
+  void itemButtonClick(CharacterInfo characterInfo) {
+    CharacterDetailsDialog.show(
+      characterInfo: characterInfo,
+      clickCallback: () {
+        if (characterInfo.isVip == true && characterInfo.isLock == true) {
+          unlockCharacter(characterInfo);
+        } else if (characterInfo.isAdd == false) {
+          addCharacter(characterInfo);
+        }
+      },
+    );
+  }
+
+  void addCharacter(CharacterInfo characterInfo) {
+    charactersRepository
+        .characterAdd(
+          characterId: characterInfo.id.toString(),
+          keyboardId: currentKeyboardInfo.id.toString(),
+        )
+        .then((characterAddResponse) {
+          int index = characterList.indexWhere(
+            (element) => element.id == characterAddResponse.characterInfo.id,
+          );
+          if (index != -1) {
+            characterList[index] = characterAddResponse.characterInfo;
+          }
+          ToastUtil.show('添加成功~');
+        })
+        .catchError((error) {
+          if (error is ServerErrorException && error.code == 1005) {
+            ToastUtil.show('请开通会员解锁权益~');
+          }if (error is ServerErrorException && error.code == 1001) {
+            ToastUtil.show('键盘人设已达上限,请取消其他人设再添加~');
+          } 
+          else {
+            ErrorHandler.toastError(error);
+          }
+        });
+  }
+
+  void unlockCharacter(CharacterInfo characterInfo) {
+    charactersRepository
+        .characterUnlock(
+          characterId: characterInfo.id.toString(),
+          keyboardId: currentKeyboardInfo.id.toString(),
+        )
+        .then((characterUnlockResponse) {
+          int index = characterList.indexWhere(
+            (element) => element.id == characterUnlockResponse.characterInfo.id,
+          );
+          if (index != -1) {
+            characterList[index] = characterUnlockResponse.characterInfo;
+          }
+          ToastUtil.show('解锁成功~');
+        })
+        .catchError((error) {
+          if (error is ServerErrorException && error.code == 1005) {
+            ToastUtil.show('请开通会员解锁权益~');
+          } else {
+            ErrorHandler.toastError(error);
+          }
+        });
+  }
+}

+ 228 - 0
lib/dialog/content/character_tab_group_content_view.dart

@@ -0,0 +1,228 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:easy_refresh/easy_refresh.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:get/get.dart';
+import 'package:get_it/get_it.dart';
+import 'package:keyboard/base/base_view.dart';
+import 'package:keyboard/resource/string.gen.dart';
+
+import '../../../data/bean/character_info.dart';
+import '../../../resource/assets.gen.dart';
+import '../../data/bean/character_group_info.dart';
+import '../../data/bean/keyboard_info.dart';
+import '../../data/repository/characters_repository.dart';
+import 'character_tab_group_content_controller.dart';
+
+class CharacterTabGroupContentView
+    extends BaseView<CharacterTabGroupContentController> {
+  final CharacterGroupInfo characterGroupInfo;
+  final KeyboardInfo currentKeyboardInfo;
+
+  const CharacterTabGroupContentView({
+    super.key,
+    required this.characterGroupInfo,
+    required this.currentKeyboardInfo,
+  });
+
+  @override
+  String? get tag =>
+      "CharacterTabGroupContentController KeyId${currentKeyboardInfo.id}GroupId${characterGroupInfo.id}";
+
+  @override
+  backgroundColor() => const Color(0xFFF4F2FB);
+
+  @override
+  Widget buildBody(BuildContext context) {
+    Get.delete<CharacterTabGroupContentController>(
+      tag: tag,
+    ); // **删除旧 Controller**
+    Get.lazyPut(
+      () => CharacterTabGroupContentController(
+        GetIt.instance.get<CharactersRepository>(),
+        currentCharacterGroupInfo: characterGroupInfo,
+        currentKeyboardInfo: currentKeyboardInfo,
+      ),
+      tag: tag,
+    );
+
+    return Column(
+      children: [
+        Expanded(
+          child: Obx(() {
+            return EasyRefresh(
+              controller: controller.refreshController,
+              header: const ClassicHeader(),
+              footer: ClassicFooter(
+                noMoreText: StringName.noMoreData,
+                failedText: StringName.loadFailed,
+                processedText: StringName.loadCompleted,
+                processingText: StringName.loading,
+              ),
+
+              // onRefresh: controller.refreshData,
+              onLoad: controller.loadMoreData,
+              child: ListView.separated(
+                itemCount: controller.characterList.length,
+                itemBuilder: (context, index) {
+                  return _buildListItem(
+                    characterInfo: controller.characterList[index],
+                  );
+                },
+                separatorBuilder: (BuildContext context, int index) {
+                  return SizedBox(
+                    width: double.infinity,
+                    height: 10.h,
+                    child: Container(color: const Color(0xFFF4F2FB)),
+                  );
+                },
+              ),
+            );
+          }),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildListItem({required CharacterInfo characterInfo}) {
+    return GestureDetector(
+      onTap: () {
+        controller.itemButtonClick(characterInfo);
+      },
+      child: Container(
+        padding: EdgeInsets.all(14.r),
+        decoration: ShapeDecoration(
+          color: Colors.white,
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(12.r),
+          ),
+        ),
+        height: 88.h,
+        child: Row(
+          children: [
+            _buildAvatar(imageUrl: characterInfo.imageUrl),
+            SizedBox(width: 8.w),
+            _buildCharacterInfo(characterInfo),
+            _buildActionButton(characterInfo),
+          ],
+        ),
+      ),
+    );
+  }
+
+  /// 角色头像
+  Widget _buildAvatar({required String? imageUrl}) {
+    return Container(
+      width: 60.r,
+      height: 60.r,
+      decoration: BoxDecoration(
+        borderRadius: BorderRadius.circular(8),
+        gradient: LinearGradient(
+          begin: Alignment.topCenter,
+          end: Alignment.bottomCenter,
+          colors: [Color(0xffebe6ff), Color(0xffffe6fe)],
+        ),
+      ),
+      child: CachedNetworkImage(
+        imageUrl: imageUrl ?? "",
+        width: 60.r,
+        height: 60.r,
+        fit: BoxFit.cover,
+      ),
+    );
+  }
+
+  /// 构建角色信息,包括名称、VIP标识和描述
+  Widget _buildCharacterInfo(CharacterInfo characterInfo) {
+    return Expanded(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            children: [
+              Text(
+                characterInfo.name ?? "",
+                style: TextStyle(
+                  color: Colors.black.withAlpha(204),
+                  fontSize: 15.sp,
+                  fontWeight: FontWeight.w500,
+                ),
+              ),
+              SizedBox(width: 4.w),
+              characterInfo.isVip == true
+                  ? Assets.images.iconCharacterVip.image(
+                    width: 38.w,
+                    height: 16.h,
+                  )
+                  : Container(),
+            ],
+          ),
+          Text(
+            characterInfo.description ?? "",
+            softWrap: true,
+            style: TextStyle(
+              color: Colors.black.withAlpha(153),
+              fontSize: 12.sp,
+              fontWeight: FontWeight.w400,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  ///  按钮
+  Widget _buildActionButton(CharacterInfo characterInfo) {
+    return InkWell(
+      onTap: () {
+        controller.itemButtonClick(characterInfo);
+      },
+      child: Container(
+        width: 72.w,
+        height: 28.h,
+        margin: EdgeInsets.only(left: 8.w),
+        decoration: BoxDecoration(
+          borderRadius: BorderRadius.circular(50.r),
+          gradient:
+              characterInfo.isAdd == true
+                  ? null
+                  : const LinearGradient(
+                    colors: [Color(0xFF7D46FC), Color(0xFFBC87FF)],
+                    begin: Alignment.topLeft,
+                    end: Alignment.bottomRight,
+                  ),
+          color: characterInfo.isAdd == true ? const Color(0xFFEDE8FF) : null,
+        ),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            if (characterInfo.isLock == true && characterInfo.isVip == true)
+              Padding(
+                padding: EdgeInsets.only(right: 2.w),
+                child: Assets.images.iconCharacterLock.image(
+                  width: 18.r,
+                  height: 18.r,
+                ), // 锁定图标
+              ),
+            Text(
+              characterInfo.isLock == true && characterInfo.isVip == true
+                  ? StringName.characterUnlock
+                  : characterInfo.isAdd == true
+                  ? StringName.characterAdded
+                  : StringName.characterAdd,
+              style: TextStyle(
+                color:
+                    characterInfo.isAdd == true
+                        ? const Color(0xFF7D46FC)
+                        : Colors.white,
+                fontSize: 14.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 144 - 0
lib/dialog/custom_label_dialog.dart

@@ -0,0 +1,144 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:keyboard/resource/string.gen.dart';
+import 'package:keyboard/utils/toast_util.dart';
+
+import '../resource/assets.gen.dart';
+import '../resource/colors.gen.dart';
+import '../utils/styles.dart';
+
+class CustomLabelDialog {
+  static const String tag = 'CustomLabelDialog';
+
+  static void show({
+    required Function(String) clickCallback,
+    required String hintText,
+    required int maxLength,
+  }) {
+    TextEditingController textController = TextEditingController();
+    SmartDialog.show(
+      tag: tag,
+      backType: SmartBackType.block,
+      clickMaskDismiss: true,
+      maskColor: ColorName.black70,
+      builder: (_) {
+        return Scaffold(
+          backgroundColor: Colors.transparent,
+            body: Column(
+          crossAxisAlignment: CrossAxisAlignment.center,
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [Container(
+            width: double.infinity,
+            margin: EdgeInsets.symmetric(horizontal: 31.w),
+            decoration: ShapeDecoration(
+              color: Colors.white,
+              shape: RoundedRectangleBorder(
+                borderRadius: BorderRadius.circular(20.r),
+              ),
+            ),
+            child: Stack(
+              children: [
+                Container(
+                  padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 24.h),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    crossAxisAlignment: CrossAxisAlignment.center,
+                    mainAxisAlignment: MainAxisAlignment.center,
+                    children: [
+                      Text(
+                        StringName.customLabel,
+                        style: TextStyle(
+                          color: Colors.black.withAlpha(204),
+                          fontSize: 16.sp,
+                          fontWeight: FontWeight.w500,
+                        ),
+                      ),
+                      SizedBox(height: 16.h),
+                      Container(
+                        height: 48.h,
+
+                        alignment: Alignment.center,
+                        decoration: ShapeDecoration(
+                          color: const Color(0xFFF5F4F9),
+                          shape: RoundedRectangleBorder(
+                            borderRadius: BorderRadius.circular(31.r),
+                          ),
+                        ),
+                        child: TextField(
+                          controller: textController,
+                          // maxLength: maxLength,
+                          maxLines: null,
+                          expands: true,
+                          textAlign: TextAlign.center,
+                          textAlignVertical: TextAlignVertical.center,
+                          decoration: InputDecoration(
+                            counterText: "",
+                            hintText: hintText,
+                            hintStyle: TextStyle(
+                              color: Colors.black.withAlpha(66),
+                            ),
+                            border: OutlineInputBorder(
+                              borderRadius: BorderRadius.circular(10.r),
+                              borderSide: BorderSide.none, // 移除边框线
+                            ),
+                            filled: true,
+                            fillColor: const Color(0xFFF5F4F9),
+                          ),
+                        ),
+                      ),
+                      SizedBox(height: 24.h),
+                      Container(
+                        height: 48.h,
+                        width: double.infinity,
+                        decoration: Styles.getActivateButtonDecoration(31.r),
+                        child: TextButton(
+                          onPressed: () {
+                            if (textController.text.isEmpty) {
+                              return ToastUtil.show(hintText);
+                            }
+                            if (textController.text.length > maxLength) {
+                              return ToastUtil.show('最多$maxLength个字哦~');
+                            }
+
+                            clickCallback(textController.text.trim());
+                            SmartDialog.dismiss();
+                          },
+                          child: Text(
+                            StringName.customLabelSave,
+                            style: TextStyle(
+                              color: Colors.white,
+                              fontSize: 16.sp,
+                              fontWeight: FontWeight.w500,
+                            ),
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                ),
+                Positioned(
+                  right: 14,
+                  top: 14,
+
+                  child: GestureDetector(
+                    onTap: () {
+                      SmartDialog.dismiss();
+                    },
+                    child: Assets.images.iconCustomDialogClose.image(
+                      width: 24.w,
+                      height: 24.h,
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),]
+        ));
+      },
+    );
+  }
+}

+ 201 - 7
lib/module/character_custom/character_custom_controller.dart

@@ -1,20 +1,214 @@
+import 'package:flutter/cupertino.dart';
+import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/data/bean/custom_config_info.dart';
+import 'package:keyboard/data/repository/config_repository.dart';
+import 'package:keyboard/module/character_custom/detail/character_custom_detail_page.dart';
+import 'package:keyboard/resource/string.gen.dart';
 import 'package:keyboard/utils/atmob_log.dart';
-import 'package:get/get.dart';
+
+import '../../dialog/custom_label_dialog.dart';
+import '../../utils/toast_util.dart';
+
+enum StepType {
+  home(0),
+  hobbies(1),
+  characters(2),
+  inputName(3);
+
+  final int value;
+
+  const StepType(this.value);
+}
+
 @injectable
 class CharacterCustomController extends BaseController {
   final String tag = 'CharacterCustomController';
+  final ConfigRepository configRepository;
+
+  final Rxn<CustomConfigInfo> currentCharacterCustomConfig =
+      Rxn<CustomConfigInfo>();
+
+  final RxList<Hobbies> hobbiesLabelsList = <Hobbies>[].obs;
+  final RxList<String> hobbiesSelectLabels = <String>[].obs;
+
+  final RxList<CharactersList> characterLabelsList = <CharactersList>[].obs;
+  final RxList<String> characterSelectLabels = <String>[].obs;
+
+  final Rx<StepType> currentStep = StepType.home.obs;
+
+  final RxString currentNameValue = "".obs;
+
+  CharacterCustomController(this.configRepository);
+
+
+
+  @override
+  void onInit() {
+    super.onInit();
+
+    AtmobLog.d(tag, "首次加载数据,触发 refreshCharacterCustomConfig()");
+    refreshCharacterCustomConfig();
+  }
+
+  //  初始化数据
 
-  CharacterCustomController();
-  var interests = <String>[].obs;
-  clickStartCustom() {
-    AtmobLog.d(tag, "clickStartCustom");
+  // 自定义兴趣爱好
+  void clickHobbiesCustom() {
+    AtmobLog.d(tag, "clickHobbiesCustom");
+    CustomLabelDialog.show(
+      maxLength: currentCharacterCustomConfig.value?.maxHobbyWords ?? 10,
+      hintText: StringName.customLabelHobbiesHint,
+      clickCallback: (value) {
+        hobbiesLabelsList.add(Hobbies(name: value));
+      },
+    );
+  }
 
+  // 自定义兴趣爱好
+  void clickCharacterCustom() {
+    AtmobLog.d(tag, "clickCharacterCustom");
+    CustomLabelDialog.show(
+      maxLength: currentCharacterCustomConfig.value?.maxCharacterWords ?? 10,
+      hintText: StringName.customLabelCharacterHint,
+      clickCallback: (value) {
+        characterLabelsList.add(CharactersList(name: value));
+      },
+    );
   }
 
-  clickBack() {
+  /// 返回上一页
+  void clickBack() {
     AtmobLog.d(tag, "clickBack");
-    Get.back();
+    if (currentStep.value == StepType.hobbies) {
+      currentStep.value = StepType.home;
+    } else if (currentStep.value == StepType.characters) {
+      currentStep.value = StepType.hobbies;
+    } else if (currentStep.value == StepType.inputName) {
+      currentStep.value = StepType.characters;
+    } else {
+      Get.back();
+    }
+  }
+
+  Future<void> refreshCharacterCustomConfig() async {
+    try {
+      final value = await configRepository.getCharacterCustomConfig();
+      currentCharacterCustomConfig.value = value.characterInfo;
+      hobbiesLabelsList.value = value.characterInfo?.hobbies ?? [];
+      characterLabelsList.value = value.characterInfo?.characters ?? [];
+    } catch (e) {
+      AtmobLog.e(tag, "获取定制人设配置失败: $e");
+    }
+  }
+
+  // 爱好页的下一步
+  void clickHobbiesNext() {
+    int min = currentCharacterCustomConfig.value?.minHobbyNum ?? 1;
+    int max = currentCharacterCustomConfig.value?.maxHobbyNum ?? 3;
+
+    if (hobbiesSelectLabels.isEmpty) {
+      ToastUtil.show("请选择爱好");
+      return;
+    }
+    if (hobbiesSelectLabels.length < min) {
+      ToastUtil.show("至少选择$min个爱好");
+      return;
+    }
+    if (hobbiesSelectLabels.length > max) {
+      ToastUtil.show("最多选择$max个爱好");
+      return;
+    }
+    clickNextButton(StepType.characters);
+  }
+
+  // 性格页的下一步
+  void clickCharacterNext() {
+    int min = currentCharacterCustomConfig.value?.minCharacterNum ?? 1;
+    int max = currentCharacterCustomConfig.value?.maxCharacterNum ?? 3;
+
+    if (characterSelectLabels.isEmpty) {
+      ToastUtil.show("请选择特质");
+      return;
+    }
+    if (characterSelectLabels.length < min) {
+      ToastUtil.show("至少选择$min个特质");
+      return;
+    }
+    if (characterSelectLabels.length > max) {
+      ToastUtil.show("最多选择$max个特质");
+      return;
+    }
+    clickNextButton(StepType.inputName);
+  }
+
+  // 名字页的下一步
+  void clickInputNameNext() {
+    if (currentNameValue.value.isEmpty) {
+      ToastUtil.show("请输入名字");
+      return;
+    }
+    if (currentNameValue.value.length > 5) {
+      ToastUtil.show("最多5个字哦~");
+      return;
+    }
+
+    CharacterCustomDetailPage.start();
+  }
+
+  // 处理下一步
+  void clickNextButton(StepType stepType) {
+    if (currentCharacterCustomConfig.value == null) {
+      AtmobLog.e(tag, "clickStartCustom - 当前配置为空");
+      return;
+    }
+    if (stepType == StepType.hobbies) {
+      currentStep.value = StepType.hobbies;
+    } else if (stepType == StepType.characters) {
+      currentStep.value = StepType.characters;
+    } else if (stepType == StepType.inputName) {
+      currentStep.value = StepType.inputName;
+    } else {
+      currentStep.value = StepType.home;
+    }
+  }
+
+  /// 选择爱好标签
+  void selectHobby(String name) {
+    handleSelection(
+      name: name,
+      selectedList: hobbiesSelectLabels,
+      max: currentCharacterCustomConfig.value?.maxHobbyNum ?? 3,
+      errorMessage:
+          "最多选择${currentCharacterCustomConfig.value?.maxHobbyNum ?? 3}个爱好",
+    );
+  }
+
+  /// 选择性格标签
+  void selectCharacter(String name) {
+    handleSelection(
+      name: name,
+      selectedList: characterSelectLabels,
+      max: currentCharacterCustomConfig.value?.maxCharacterNum ?? 3,
+      errorMessage:
+          "最多选择${currentCharacterCustomConfig.value?.maxCharacterNum ?? 3}个特质",
+    );
+  }
+
+  ///标签选择处理
+  void handleSelection({
+    required String name,
+    required RxList<String> selectedList,
+    required int max,
+    required String errorMessage,
+  }) {
+    if (selectedList.contains(name)) {
+      selectedList.remove(name);
+    } else if (selectedList.length < max) {
+      selectedList.add(name);
+    } else {
+      ToastUtil.show(errorMessage);
+    }
   }
 }

+ 339 - 103
lib/module/character_custom/character_custom_page.dart

@@ -1,11 +1,14 @@
+import 'package:dotted_border/dotted_border.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_screenutil/flutter_screenutil.dart';
 import 'package:get/get.dart';
 import 'package:keyboard/base/base_page.dart';
 import 'package:keyboard/module/character_custom/character_custom_controller.dart';
 import 'package:keyboard/resource/string.gen.dart';
+import 'package:keyboard/utils/toast_util.dart';
 
 import '../../resource/assets.gen.dart';
+import '../../utils/styles.dart';
 
 class CharacterCustomPage extends BasePage<CharacterCustomController> {
   const CharacterCustomPage({super.key});
@@ -26,10 +29,17 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
 
   @override
   Widget buildBody(BuildContext context) {
-    return buildHappyPage();
+    return Obx(() {
+      if (controller.currentStep.value == StepType.home) {
+        return _buildCustomHomePage();
+      } else {
+        return _buildStepsPage();
+      }
+    });
   }
 
-  Widget buildCustomHomePage() {
+  // 定制首页
+  Widget _buildCustomHomePage() {
     return Stack(
       children: [
         Assets.images.bgCharacterCustomHuman.image(
@@ -69,7 +79,7 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
                   ),
                   child: Center(
                     child: Text(
-                      '定制历史',
+                      StringName.characterCustomHistory,
                       style: TextStyle(
                         color: Colors.white,
                         fontSize: 14.sp,
@@ -89,7 +99,7 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
           right: 0,
           child: GestureDetector(
             onTap: () {
-              controller.clickStartCustom();
+              controller.clickNextButton(StepType.hobbies);
             },
             child: Center(
               child: Assets.images.iconCharacterCustomButton.image(
@@ -103,40 +113,48 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
     );
   }
 
-  Widget buildHappyPage() {
-    return Container(
-      child: Stack(
-        children: [
-          Assets.images.bgCharacterCustomSteps.image(
-            width: double.infinity,
-            fit: BoxFit.fill,
-          ),
-          SafeArea(
-            child: Column(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                Padding(
-                  padding: EdgeInsets.only(left: 16.w),
-                  child: GestureDetector(
-                    onTap: () {
-                      controller.clickBack();
-                    },
-                    child: Assets.images.iconCharacterCustomClose.image(
-                      width: 24.w,
-                      height: 24.w,
-                    ),
+  Widget _buildStepsPage() {
+    return Stack(
+      children: [
+        Assets.images.bgCharacterCustomSteps.image(
+          width: double.infinity,
+          fit: BoxFit.fill,
+        ),
+        SafeArea(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Padding(
+                padding: EdgeInsets.only(left: 16.w, bottom: 48.h),
+                child: GestureDetector(
+                  onTap: () {
+                    controller.clickBack();
+                  },
+                  child: Assets.images.iconCharacterCustomClose.image(
+                    width: 24.w,
+                    height: 24.w,
                   ),
                 ),
-                Expanded(
+              ),
+
+              Expanded(
+                child: SingleChildScrollView(
                   child: Column(
                     mainAxisAlignment: MainAxisAlignment.center,
                     crossAxisAlignment: CrossAxisAlignment.center,
                     children: [
-
-                      Assets.images.iconCharacterCustomStepOneTitle.image(
-                        fit: BoxFit.cover,
-                        width: 214.w,
-                      ),
+                      controller.currentStep.value == StepType.hobbies
+                          ? Assets.images.iconCharacterCustomStepOneTitle.image(
+                            fit: BoxFit.cover,
+                            width: 209.w,
+                          )
+                          : controller.currentStep.value == StepType.characters
+                          ? Assets.images.iconCharacterCustomStepTwoTitle.image(
+                            fit: BoxFit.cover,
+                            width: 168.w,
+                          )
+                          : Assets.images.iconCharacterCustomStepThreeTitle
+                              .image(fit: BoxFit.cover, width: 207.w),
                       Container(
                         margin: EdgeInsets.only(
                           left: 16.w,
@@ -185,7 +203,7 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
                               crossAxisAlignment: CrossAxisAlignment.center,
                               children: [
                                 Text(
-                                  '第1步',
+                                  '第${controller.currentStep.value.value}步',
                                   style: TextStyle(
                                     color: const Color(0xFF755AAB),
                                     fontSize: 12.sp,
@@ -213,94 +231,312 @@ class CharacterCustomPage extends BasePage<CharacterCustomController> {
                                 ),
                               ],
                             ),
+
+                            if (controller.currentStep.value ==
+                                StepType.hobbies)
+                              _buildHobbiesPage(),
+                            if (controller.currentStep.value ==
+                                StepType.characters)
+                              _buildCharacterPage(),
+                            if (controller.currentStep.value ==
+                                StepType.inputName)
+                              _buildInputNamePage(),
                           ],
                         ),
                       ),
-                      _buildInterestsPage(),
                     ],
                   ),
                 ),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildSelectionPage({
+    required String title,
+    required String subtitle,
+    required RxList<dynamic> items,
+    required RxList<String> selectedLabels,
+    required Function(String) onSelected,
+    required bool isCustomEnabled,
+    required VoidCallback onCustomClick,
+    required VoidCallback nextClick,
+    required bool isShowEmoji,
+  }) {
+    return Obx(() {
+      return Column(
+        children: [
+          Padding(
+            padding: EdgeInsets.only(top: 15.h),
+            child: Text(
+              title,
+              style: TextStyle(
+                color: const Color(0xFF755BAB),
+                fontSize: 18.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ),
+          Text(
+            subtitle,
+            style: TextStyle(
+              color: Color(0xFF755BAB).withValues(alpha: 0.6),
+              fontSize: 12.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+          Container(
+            margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
+            alignment: Alignment.center,
+            child: Wrap(
+              alignment: WrapAlignment.center,
+              spacing: 8.0.r,
+              runSpacing: 10.r,
+              children: [
+                ...items.map((item) {
+                  final emoji = item.emoji ?? "";
+                  final name = item.name ?? "";
+                  return Obx(() {
+                    bool isSelected = selectedLabels.contains(name);
+                    return Stack(
+                      children: [
+                        ChoiceChip(
+                          label: Row(
+                            mainAxisSize: MainAxisSize.min,
+                            children: [
+                              Text(
+                                isShowEmoji ? "$emoji$name" : name,
+                                style: TextStyle(
+                                  color:
+                                      isSelected
+                                          ? Color(0xFFF5F4F9)
+                                          : Color(0xFF755BAB),
+                                  fontSize: 14.sp,
+                                  fontWeight: FontWeight.w400,
+                                ),
+                              ),
+                            ],
+                          ),
+                          showCheckmark: false,
+                          selected: isSelected,
+                          selectedColor: Color(0xFFB782FF),
+                          backgroundColor: Colors.white,
+                          shape: RoundedRectangleBorder(
+                            side: BorderSide(
+                              width: 1.w,
+                              color: const Color(0x4C755BAB),
+                            ),
+                            borderRadius: BorderRadius.circular(31.r),
+                          ),
+                          onSelected: (selected) {
+                            onSelected(name);
+                          },
+                        ),
+                      ],
+                    );
+                  });
+                }),
+                Visibility(
+                  visible: isCustomEnabled,
+                  child: GestureDetector(
+                    onTap: onCustomClick,
+                    child: Container(
+                      margin: EdgeInsets.only(top: 3.h),
+                      child: DottedBorder(
+                        color: const Color(0xFFC9C2DB),
+                        strokeWidth: 1.0.w,
+                        borderType: BorderType.RRect,
+                        radius: Radius.circular(20.r),
+                        child: Container(
+                          width: 86.w,
+                          height: 33.h,
+                          alignment: Alignment.center,
+                          child: Row(
+                            mainAxisAlignment: MainAxisAlignment.center,
+                            children: [
+                              Assets.images.iconCharacterCustomPlus.image(
+                                width: 18.w,
+                                height: 18.w,
+                              ),
+                              Text(
+                                StringName.characterCustomCustomizable,
+                                style: TextStyle(
+                                  color: const Color(0xFFC9C2DB),
+                                  fontSize: 14.sp,
+                                  fontWeight: FontWeight.w500,
+                                ),
+                              ),
+                            ],
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
               ],
             ),
           ),
+          Container(
+            margin: EdgeInsets.only(top: 107.h, bottom: 32.h),
+            child: _buildNextButton(
+              isEnable: selectedLabels.isNotEmpty,
+              onTap: () {
+                nextClick();
+              },
+            ),
+          ),
         ],
+      );
+    });
+  }
+
+  Widget _buildNextButton({required VoidCallback onTap, required isEnable}) {
+    return GestureDetector(
+      onTap: () {
+        onTap();
+      },
+      child: Container(
+        width: 220.w,
+        height: 48.h,
+        decoration:
+            isEnable
+                ? Styles.getActivateButtonDecoration(31.r)
+                : Styles.getInactiveButtonDecoration(31.r),
+        child: Center(
+          child: Text(
+            '下一步',
+            style: TextStyle(
+              color: Colors.white,
+              fontSize: 16.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+        ),
       ),
     );
   }
-  Widget _buildInterestsPage() {
-    List<Map<String, String>> interestOptions = [
-      {'emoji': '🐱', 'label': '撸猫'},
-      {'emoji': '🐶', 'label': '撸狗'},
-      {'emoji': '📷', 'label': '拍照摄影'},
-      {'emoji': '🔍', 'label': '美食烹饪'},
-      {'emoji': '👗', 'label': '潮流穿搭'},
-      {'emoji': '🎮', 'label': '竞技游戏'},
-      {'emoji': '🎵', 'label': '潮流穿搭潮流穿搭潮流穿搭潮流穿搭'},
-      {'emoji': '✈️', 'label': '旅游'},
-    ];
 
-    return Column(
-      children: [
-        Text('你的兴趣爱好是什么?'),
-        Align(
-          alignment: Alignment.center,
-          child: Wrap(
-            alignment: WrapAlignment.center,
-            spacing: 8.0,
-            runSpacing: 8.0,
-            children: [
-              ...interestOptions.map((item) {
-                String emoji = item['emoji']!;
-                String label = item['label']!;
-                return Obx(() => ChoiceChip(
-                  label: Row(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      Text(emoji, style: TextStyle(fontSize: 18)),
-                      SizedBox(width: 4),
-                      Text(label),
-                    ],
-                  ),
-                  selected: controller.interests.contains(label),
-                  selectedColor: Colors.purple.shade100,
-                  backgroundColor: Colors.white,
-                  shape: RoundedRectangleBorder(
-                    side: BorderSide(color: Colors.purple.shade300),
-                    borderRadius: BorderRadius.circular(20),
-                  ),
-                  onSelected: (selected) {
-                    if (selected && controller.interests.length < 3) {
-                      controller.interests.add(label);
-                    } else if (!selected) {
-                      controller.interests.remove(label);
-                    }
-                  },
-                ));
-              }).toList(),
-              GestureDetector(
-                onTap: () {
-                  // TODO: 处理自定义兴趣的逻辑,例如弹出输入框
+  // 选择爱好页面
+  Widget _buildHobbiesPage() {
+    return _buildSelectionPage(
+      title: StringName.characterCustomHobbiesTitle,
+      subtitle:
+          "(最多选择${controller.currentCharacterCustomConfig.value?.maxHobbyNum ?? 3}个)",
+      items: controller.hobbiesLabelsList,
+      selectedLabels: controller.hobbiesSelectLabels,
+      isShowEmoji: true,
+      onSelected: (name) {
+        controller.selectHobby(name);
+      },
+      isCustomEnabled:
+          controller.currentCharacterCustomConfig.value?.customHobby == true,
+      onCustomClick: () {
+        controller.clickHobbiesCustom();
+      },
+      nextClick: () {
+        controller.clickHobbiesNext();
+      },
+    );
+  }
+
+  // 选择性格页面
+  Widget _buildCharacterPage() {
+    return _buildSelectionPage(
+      title: StringName.characterCustomcharacterTitle,
+      subtitle:
+          "(最多选择${controller.currentCharacterCustomConfig.value?.maxCharacterNum ?? 3}个)",
+      items: controller.characterLabelsList,
+      selectedLabels: controller.characterSelectLabels,
+      isShowEmoji: false,
+      onSelected: (name) {
+        controller.selectCharacter(name);
+      },
+      isCustomEnabled:
+          controller.currentCharacterCustomConfig.value?.customCharacter ==
+          true,
+      onCustomClick: () {
+        controller.clickCharacterCustom();
+      },
+      nextClick: () {
+        controller.clickCharacterNext();
+      },
+    );
+  }
+
+  //  输入名字页面
+  Widget _buildInputNamePage() {
+    return Obx(() {
+      return Column(
+        children: [
+          Padding(
+            padding: EdgeInsets.only(top: 15.h),
+            child: Text(
+              StringName.characterCustomNameTitle,
+              style: TextStyle(
+                color: const Color(0xFF755BAB),
+                fontSize: 18.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ),
+          Text(
+            "人设名称(最多5个字)",
+            style: TextStyle(
+              color: Color(0xFF755BAB).withValues(alpha: 0.6),
+              fontSize: 12.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+          Container(
+            margin: EdgeInsets.only(top: 32.h, left: 21.w, right: 21.w),
+            alignment: Alignment.center,
+            child: Container(
+              height: 48.h,
+              alignment: Alignment.center,
+              decoration: ShapeDecoration(
+                color: const Color(0xFFF5F4F9),
+                shape: RoundedRectangleBorder(
+                  borderRadius: BorderRadius.circular(31.r),
+                ),
+              ),
+              child: TextField(
+                // maxLength: maxLength,
+                maxLines: null,
+                expands: true,
+                textAlign: TextAlign.center,
+                textAlignVertical: TextAlignVertical.center,
+                onChanged: (value) {
+                  controller.currentNameValue.value = value;
                 },
-                child: Container(
-                  padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
-                  decoration: BoxDecoration(
-                    border: Border.all(color: Colors.purple.shade300),
-                    borderRadius: BorderRadius.circular(20),
-                  ),
-                  child: Row(
-                    mainAxisSize: MainAxisSize.min,
-                    children: [
-                      Icon(Icons.add, size: 18, color: Colors.purple.shade300),
-                      SizedBox(width: 4),
-                      Text('自定义', style: TextStyle(color: Colors.purple.shade300)),
-                    ],
+                decoration: InputDecoration(
+                  counterText: "",
+                  hintText: StringName.characterCustomNameHint,
+                  hintStyle: TextStyle(color: Colors.black.withAlpha(66)),
+                  border: OutlineInputBorder(
+                    borderRadius: BorderRadius.circular(31.r),
+                    borderSide: BorderSide.none, // 移除边框线
                   ),
+
+                  filled: true,
+                  fillColor: const Color(0xFFF5F4F9),
                 ),
               ),
-            ],
+            ),
           ),
-        ),
-      ],
-    );
+          Container(
+            margin: EdgeInsets.only(top: 44.h, bottom: 32.h),
+            child: _buildNextButton(
+              isEnable: controller.currentNameValue.trim().isNotEmpty,
+              onTap: () {
+                controller.clickInputNameNext();
+              },
+            ),
+          ),
+        ],
+      );
+    });
   }
 }

+ 40 - 0
lib/module/character_custom/detail/character_custom_detail_controller.dart

@@ -0,0 +1,40 @@
+import 'package:injectable/injectable.dart';
+import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/data/repository/characters_repository.dart';
+import 'package:get/get.dart';
+
+@injectable
+class CharacterCustomDetailController extends BaseController {
+  final CharactersRepository charactersRepository;
+
+  CharacterCustomDetailController(this.charactersRepository);
+
+  @override
+  void onInit() {
+    super.onInit();
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+  }
+
+  @override
+  void onClose() {
+    super.onClose();
+  }
+
+  void clickBack() {
+    Get.back();
+  }
+
+  void clickUnlockButton() {
+
+  }
+
+  // 生成定制人设
+  Future<void>generateCharacterCustom() async {
+
+
+  }
+}

+ 358 - 0
lib/module/character_custom/detail/character_custom_detail_page.dart

@@ -0,0 +1,358 @@
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:keyboard/base/base_page.dart';
+import 'package:flutter/material.dart';
+import '../../../resource/assets.gen.dart';
+import '../../../resource/string.gen.dart';
+import '../../../router/app_pages.dart';
+import '../../../utils/styles.dart';
+import 'character_custom_detail_controller.dart';
+import 'package:get/get.dart';
+
+class CharacterCustomDetailPage
+    extends BasePage<CharacterCustomDetailController> {
+  const CharacterCustomDetailPage({super.key});
+
+  static void start() {
+    Get.toNamed(RoutePath.characterCustomDetail, arguments: {});
+  }
+
+  @override
+  bool immersive() {
+    return true;
+  }
+
+  @override
+  Widget buildBody(BuildContext context) {
+    return Stack(
+      children: [
+        Container(
+          child: Assets.images.bgCharacterCustomDetail.image(
+            width: double.infinity,
+            fit: BoxFit.fill,
+          ),
+        ),
+        SafeArea(
+          child: Container(
+            color: Colors.transparent,
+            alignment: Alignment.topCenter,
+            child: Stack(
+              children: [
+                Column(
+                  children: [
+                    _buildTitle(),
+                    SizedBox(height: 42.h),
+                    Expanded(
+                      child: Container(
+                        decoration: ShapeDecoration(
+                          color: Color(0xFFF6F5FA),
+                          shape: RoundedRectangleBorder(
+                            borderRadius: BorderRadius.only(
+                              topLeft: Radius.circular(20.r),
+                              topRight: Radius.circular(20.r),
+                            ),
+                          ),
+                        ),
+                        child: SingleChildScrollView(
+                          child: Column(
+                            children: [
+                              _buildNameCard(),
+                              SizedBox(height: 34.h),
+                              _buildGenderCard(),
+                              SizedBox(height: 10.h),
+                              _buildBirthdayCard(),
+                              SizedBox(height: 10.h),
+                              _hobbiesCard(),
+                              SizedBox(height: 18.h),
+                              _characterCard(),
+                            ],
+                          ),
+                        ),
+                      ),
+                    ),
+                    Container(
+                      color: Color(0xFFF6F5FA),
+                      child: _buildUnlockButton(),
+                    ),
+                  ],
+                ),
+                Positioned(left: 16.w, top: 60.h, child: _buildAvatar()),
+                Positioned(left: 68.w, top: 112.h, child: _buildAvatarSwitch()),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  _buildTitle() {
+    return Container(
+      alignment: Alignment.centerLeft,
+      padding: EdgeInsets.only(top: 12.h, left: 16.w),
+      child: GestureDetector(
+        onTap: controller.clickBack,
+        child: Assets.images.iconMineBackArrow.image(width: 24.w, height: 24.w),
+      ),
+    );
+  }
+
+  _buildNameCard() {
+    return Container(
+      padding: EdgeInsets.only(left: 104.w, top: 14.h),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.end,
+        children: [
+          Text(
+            '自定义人设',
+            textAlign: TextAlign.center,
+            style: TextStyle(
+              color: Colors.black.withAlpha(204),
+              fontSize: 18.sp,
+              fontWeight: FontWeight.w500,
+            ),
+          ),
+          Container(
+            child: Assets.images.iconCharacterCustomDetailEdit.image(
+              width: 20.r,
+              height: 20.r,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+
+  _buildAvatar() {
+    return Container(
+      width: 72.r,
+      height: 72.r,
+      decoration: ShapeDecoration(
+        shape: OvalBorder(side: BorderSide(width: 2, color: Colors.white)),
+      ),
+    );
+  }
+
+  _buildAvatarSwitch() {
+    return SizedBox(
+      width: 22.r,
+      height: 22.r,
+      child: Assets.images.iconCharacterCustomDetailSwitch.image(
+        width: 22.r,
+        height: 22.r,
+      ),
+    );
+  }
+
+  // 性别
+  Widget _buildGenderCard() {
+    return _buildListItem(
+      onTap: () {
+        debugPrint('点击了性别');
+      },
+      firstWidget: Text('性别', style: Styles.getTextStyleBlack204W400(14.sp)),
+      bottomWidget: Row(
+        children: [
+          Assets.images.iconCharacterCustomDetailMale.image(
+            width: 24.w,
+            height: 24.w,
+          ),
+          SizedBox(width: 6.w),
+          Text('男', style: Styles.getTextStyleBlack204W400(14.sp)),
+          Spacer(),
+          Assets.images.iconArrowRight.image(width: 24.w, height: 24.w),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildBirthdayCard() {
+    return _buildListItem(
+      onTap: () {
+        debugPrint('点击了生日');
+      },
+      firstWidget: Text('出生日期', style: Styles.getTextStyleBlack204W400(14.sp)),
+      bottomWidget: Row(
+        children: [
+          Text('1998-12-16', style: Styles.getTextStyleBlack204W400(14.sp)),
+          SizedBox(width: 12.w),
+          Text('22岁', style: Styles.getTextStyleBlack204W400(14.sp)),
+          Spacer(),
+          Assets.images.iconArrowRight.image(width: 24.w, height: 24.w),
+        ],
+      ),
+    );
+  }
+
+  Widget _hobbiesCard() {
+    return _buildListItem(
+      onTap: () {
+        debugPrint('点击了爱好');
+      },
+      firstWidget: Text('兴趣爱好', style: Styles.getTextStyleBlack204W400(14.sp)),
+      bottomWidget: Row(
+        children: [
+          Expanded(
+            child: Wrap(
+              spacing: 8.w,
+              runSpacing: 8.h,
+              children: [
+                _buildColorTag(
+                  color: Color(0xFFFEECE0),
+                  name: '45sssssssssssssssssss摄影',
+                ),
+                _buildColorTag(
+                  color: Color(0xFFE8E4FC),
+                  name: '摄影摄影摄影摄影摄影摄摄影摄影摄影',
+                ),
+                _buildColorTag(
+                  color: Color(0xFFE8E4FC),
+                  name: '摄影摄影摄影摄影摄影摄摄影摄影摄影',
+                ),
+              ],
+            ),
+          ),
+          SizedBox(width: 8.w),
+          Assets.images.iconArrowRight.image(width: 24.w, height: 24.w),
+        ],
+      ),
+    );
+  }
+
+  Widget _characterCard() {
+    return _buildListItem(
+      onTap: () {
+        debugPrint('点击了性格');
+      },
+      firstWidget: Text('性格', style: Styles.getTextStyleBlack204W400(14.sp)),
+      bottomWidget: Row(
+        children: [
+          Expanded(
+            child: Wrap(
+              spacing: 8.w,
+              runSpacing: 8.h,
+              children: [
+                _buildColorTag(color: Color(0xFFFEECE0), name: '45摄影'),
+                _buildColorTag(color: Color(0xFFE8E4FC), name: '46摄影'),
+                _buildColorTag(color: Color(0xFFE0EDFD), name: '测试一共有多长才可以'),
+              ],
+            ),
+          ),
+          SizedBox(width: 8.w),
+          Assets.images.iconArrowRight.image(width: 24.w, height: 24.w),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildUnlockButton() {
+    return GestureDetector(
+      onTap: () {
+        controller.clickUnlockButton();
+      },
+      child: Container(
+        width: double.infinity,
+        margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.h),
+        height: 48.h,
+        alignment: Alignment.center,
+        decoration: ShapeDecoration(
+          color: const Color(0xFF7D46FC),
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(50.r),
+          ),
+        ),
+        child: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Assets.images.iconCharacterCustomDetailLock.image(
+              width: 22.w,
+              height: 22.w,
+            ),
+            Text(
+              StringName.unlockExclusiveCharacter,
+              textAlign: TextAlign.center,
+              style: Styles.getTextStyleWhiteW500(16.sp),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  // 列表项
+  Widget _buildListItem({
+    required Widget firstWidget,
+    required Widget bottomWidget,
+    VoidCallback? onTap,
+  }) {
+    return Container(
+      padding: EdgeInsets.only(
+        left: 12.w,
+        right: 12.w,
+        top: 14.h,
+        bottom: 14.h,
+      ),
+      margin: EdgeInsets.only(left: 16.w, right: 16.w),
+      width: double.infinity,
+      decoration: ShapeDecoration(
+        color: Colors.white,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(12.r),
+        ),
+      ),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          firstWidget,
+          _buildDivider(),
+          GestureDetector(
+            behavior: HitTestBehavior.opaque,
+            onTap: onTap,
+            child: bottomWidget,
+          ),
+        ],
+      ),
+    );
+  }
+
+  // 下划线
+  Widget _buildDivider() {
+    return Container(
+      margin: EdgeInsets.only(top: 8.h, bottom: 8.h),
+      width: 304.w,
+      decoration: ShapeDecoration(
+        shape: RoundedRectangleBorder(
+          side: BorderSide(
+            width: 1.r,
+            strokeAlign: BorderSide.strokeAlignCenter,
+            color: const Color(0xFFF5F4F9),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildColorTag({required Color color, String? name, String? emoji}) {
+    return Container(
+      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 3.h),
+      decoration: ShapeDecoration(
+        color: color,
+        shape: RoundedRectangleBorder(
+          borderRadius: BorderRadius.circular(23.r),
+        ),
+      ),
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        mainAxisAlignment: MainAxisAlignment.center,
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          if (emoji != null)
+            Text(emoji, style: Styles.getTextStyleBlack204W400(14.sp)),
+          if (emoji != null && name != null) SizedBox(width: 4.w),
+          if (name != null)
+            Text(name, style: Styles.getTextStyleBlack204W400(14.sp)),
+        ],
+      ),
+    );
+  }
+}

+ 17 - 8
lib/module/keyboard_manage/keyboard_manage_controller.dart

@@ -214,8 +214,9 @@ class KeyboardManageController extends BaseController
           tag,
           'keyboardCharacterListResponse: ${keyboardCharacterListResponse.characterInfos.toString()}',
         );
-        _currentCustomKeyboardCharacterList.value =
-            keyboardCharacterListResponse.characterInfos;
+        _currentCustomKeyboardCharacterList.addAll(
+          keyboardCharacterListResponse.characterInfos,
+        );
         _oldCustomCharacterList = List<CharacterInfo>.from(
           _currentCustomKeyboardCharacterList,
         );
@@ -351,7 +352,6 @@ class KeyboardManageController extends BaseController
     isCustom
         ? saveCustomKeyboardCharacterList()
         : saveGeneralKeyboardCharacterList();
-
   }
 
   void saveCustomKeyboardCharacterList() {
@@ -369,7 +369,7 @@ class KeyboardManageController extends BaseController
               ToastUtil.show(StringName.keyboardSaveSuccess);
 
               CharacterGroupContentController characterGroupContentController =
-              Get.find<CharacterGroupContentController>();
+                  Get.find<CharacterGroupContentController>();
               characterGroupContentController.refreshData();
             })
             .catchError((error) {
@@ -398,7 +398,7 @@ class KeyboardManageController extends BaseController
               );
 
               CharacterGroupContentController characterGroupContentController =
-              Get.find<CharacterGroupContentController>();
+                  Get.find<CharacterGroupContentController>();
               characterGroupContentController.refreshData();
               ToastUtil.show(StringName.keyboardSaveSuccess);
             })
@@ -479,14 +479,23 @@ class KeyboardManageController extends BaseController
 
   clickAddCharacter({required bool isCustom}) {
     if (isCustom) {
-      AtmobLog.i(tag, 'clickAddCharacter');
       CharacterAddDialog.show(
+        currentKeyboardInfo: currentCustomKeyboardInfo.value,
         clickCallback: () {
-          AtmobLog.i(tag, 'clickAddCharacter');
+          getKeyboardCharacterList(
+            keyboardId: _currentCustomKeyboardInfo.value.id ?? "",
+            isCustom: true,
+          );
+
         },
       );
     } else {
-      AtmobLog.i(tag, 'clickAddCharacter');
+      CharacterAddDialog.show(
+        currentKeyboardInfo: currentGeneralKeyboardInfo.value,
+        clickCallback: () {
+          getGeneralKeyboard();
+        },
+      );
     }
   }
 

+ 2 - 2
lib/module/keyboard_manage/keyboard_manage_page.dart

@@ -40,7 +40,7 @@ class KeyboardManagePage extends BasePage<KeyboardManageController> {
           child: Column(
             children: [
               // TabBar
-              buildTitle(),
+              _buildTitle(),
               SizedBox(height: 10.h),
               Expanded(
                 child: Container(
@@ -72,7 +72,7 @@ class KeyboardManagePage extends BasePage<KeyboardManageController> {
     );
   }
 
-  Widget buildTitle() {
+  Widget _buildTitle() {
     return Padding(
       padding: EdgeInsets.symmetric(horizontal: 16.w),
       child: Row(

+ 44 - 0
lib/resource/assets.gen.dart

@@ -16,6 +16,10 @@ class $AssetsImagesGen {
   AssetGenImage get bgCharacterBoyBanner =>
       const AssetGenImage('assets/images/bg_character_boy_banner.webp');
 
+  /// File path: assets/images/bg_character_custom_detail.webp
+  AssetGenImage get bgCharacterCustomDetail =>
+      const AssetGenImage('assets/images/bg_character_custom_detail.webp');
+
   /// File path: assets/images/bg_character_custom_human.webp
   AssetGenImage get bgCharacterCustomHuman =>
       const AssetGenImage('assets/images/bg_character_custom_human.webp');
@@ -59,6 +63,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconAboutArrowLeft =>
       const AssetGenImage('assets/images/icon_about_arrow_left.webp');
 
+  /// File path: assets/images/icon_arrow_right.webp
+  AssetGenImage get iconArrowRight =>
+      const AssetGenImage('assets/images/icon_arrow_right.webp');
+
   /// File path: assets/images/icon_black_back.webp
   AssetGenImage get iconBlackBack =>
       const AssetGenImage('assets/images/icon_black_back.webp');
@@ -79,6 +87,30 @@ class $AssetsImagesGen {
   AssetGenImage get iconCharacterCustomClose =>
       const AssetGenImage('assets/images/icon_character_custom_close.webp');
 
+  /// File path: assets/images/icon_character_custom_detail_edit.webp
+  AssetGenImage get iconCharacterCustomDetailEdit => const AssetGenImage(
+    'assets/images/icon_character_custom_detail_edit.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_detail_lock.webp
+  AssetGenImage get iconCharacterCustomDetailLock => const AssetGenImage(
+    'assets/images/icon_character_custom_detail_lock.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_detail_male.webp
+  AssetGenImage get iconCharacterCustomDetailMale => const AssetGenImage(
+    'assets/images/icon_character_custom_detail_male.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_detail_switch.webp
+  AssetGenImage get iconCharacterCustomDetailSwitch => const AssetGenImage(
+    'assets/images/icon_character_custom_detail_switch.webp',
+  );
+
+  /// File path: assets/images/icon_character_custom_plus.webp
+  AssetGenImage get iconCharacterCustomPlus =>
+      const AssetGenImage('assets/images/icon_character_custom_plus.webp');
+
   /// File path: assets/images/icon_character_custom_step_one_title.webp
   AssetGenImage get iconCharacterCustomStepOneTitle => const AssetGenImage(
     'assets/images/icon_character_custom_step_one_title.webp',
@@ -126,6 +158,10 @@ class $AssetsImagesGen {
   AssetGenImage get iconCharacterVip =>
       const AssetGenImage('assets/images/icon_character_vip.webp');
 
+  /// File path: assets/images/icon_custom_dialog_close.webp
+  AssetGenImage get iconCustomDialogClose =>
+      const AssetGenImage('assets/images/icon_custom_dialog_close.webp');
+
   /// File path: assets/images/icon_dialog_close_black.webp
   AssetGenImage get iconDialogCloseBlack =>
       const AssetGenImage('assets/images/icon_dialog_close_black.webp');
@@ -235,6 +271,7 @@ class $AssetsImagesGen {
   /// List of all assets
   List<AssetGenImage> get values => [
     bgCharacterBoyBanner,
+    bgCharacterCustomDetail,
     bgCharacterCustomHuman,
     bgCharacterCustomSteps,
     bgCharacterCustomStepsDesc,
@@ -246,11 +283,17 @@ class $AssetsImagesGen {
     bgMine,
     bgMineVipCard,
     iconAboutArrowLeft,
+    iconArrowRight,
     iconBlackBack,
     iconCharacterArrowDown,
     iconCharacterArrowRight,
     iconCharacterCustomButton,
     iconCharacterCustomClose,
+    iconCharacterCustomDetailEdit,
+    iconCharacterCustomDetailLock,
+    iconCharacterCustomDetailMale,
+    iconCharacterCustomDetailSwitch,
+    iconCharacterCustomPlus,
     iconCharacterCustomStepOneTitle,
     iconCharacterCustomStepThreeTitle,
     iconCharacterCustomStepTwoTitle,
@@ -262,6 +305,7 @@ class $AssetsImagesGen {
     iconCharacterLock,
     iconCharacterMarket,
     iconCharacterVip,
+    iconCustomDialogClose,
     iconDialogCloseBlack,
     iconKeyboardManageCustom,
     iconKeyboardManageFavorite,

+ 28 - 2
lib/resource/string.gen.dart

@@ -71,7 +71,20 @@ class StringName {
   static final String keyboardSaveFailed = 'keyboard_save_failed'.tr; // 保存失败
   static final String addCharacter = 'add_character'.tr; // 添加人设
   static final String customCharacter = 'custom_character'.tr; // 定制人设
-  static final String characterCustomStepsDesc = 'character_custom_steps_desc'.tr; // 专属于你独一无二的人设
+  static final String characterCustomStepsDesc = 'character_custom_steps_desc'.tr; // 量身打造 自定义人设更贴脸
+  static final String characterCustomHobbiesTitle = 'character_custom_hobbies_title'.tr; // 用爱好开启对话,发现更多可能性
+  static final String characterCustomcharacterTitle = 'character_custom_character_title'.tr; // 最想用哪三个特质打动TA
+  static final String characterCustomNameTitle = 'character_custom_name_title'.tr; // 给人设取一个名字
+  static final String characterCustomCustomizable = 'character_custom_customizable'.tr; // 自定义
+  static final String characterCustomNextStep = 'character_custom_next_step'.tr; // 下一步
+  static final String characterCustomNameHint = 'character_custom_name_hint'.tr; // 输入你的名字
+  static final String characterCustomHistory = 'character_custom_history'.tr; // 定制历史
+  static final String customLabel = 'custom_label'.tr; // 自定义标签
+  static final String customLabelHobbiesHint = 'custom_label_hobbies_hint'.tr; // 输入你的爱好
+  static final String customLabelCharacterHint = 'custom_label_character_hint'.tr; // 输入你的特质
+  static final String customLabelNameHint = 'custom_label_name_hint'.tr; // 输入你的名字
+  static final String customLabelSave = 'custom_label_save'.tr; // 保存
+  static final String unlockExclusiveCharacter = 'unlock_exclusive_character'.tr; // 解锁专属人设
 }
 class StringMultiSource {
   StringMultiSource._();
@@ -146,7 +159,20 @@ class StringMultiSource {
       'keyboard_save_failed': '保存失败',
       'add_character': '添加人设',
       'custom_character': '定制人设',
-      'character_custom_steps_desc': '专属于你独一无二的人设',
+      'character_custom_steps_desc': '量身打造 自定义人设更贴脸',
+      'character_custom_hobbies_title': '用爱好开启对话,发现更多可能性',
+      'character_custom_character_title': '最想用哪三个特质打动TA',
+      'character_custom_name_title': '给人设取一个名字',
+      'character_custom_customizable': '自定义',
+      'character_custom_next_step': '下一步',
+      'character_custom_name_hint': '输入你的名字',
+      'character_custom_history': '定制历史',
+      'custom_label': '自定义标签',
+      'custom_label_hobbies_hint': '输入你的爱好',
+      'custom_label_character_hint': '输入你的特质',
+      'custom_label_name_hint': '输入你的名字',
+      'custom_label_save': '保存',
+      'unlock_exclusive_character': '解锁专属人设',
     },
   };
 }

+ 7 - 3
lib/router/app_pages.dart

@@ -3,20 +3,19 @@ import 'package:keyboard/module/about/about_controller.dart';
 import 'package:keyboard/module/browser/browser_controller.dart';
 import 'package:keyboard/module/character/content/character_group_content_controller.dart';
 import 'package:keyboard/module/character_custom/character_custom_controller.dart';
+import 'package:keyboard/module/character_custom/detail/character_custom_detail_page.dart';
 import 'package:keyboard/module/feedback/feedback_controller.dart';
 import 'package:keyboard/module/keyboard/keyboard_controller.dart';
 import 'package:keyboard/module/keyboard_manage/keyboard_manage_controller.dart';
-
 import 'package:keyboard/module/login/login_controller.dart';
 import 'package:keyboard/module/mine/mine_controller.dart';
 
-import '../data/bean/character_group_info.dart';
-import '../data/repository/characters_repository.dart';
 import '../di/get_it.dart';
 import '../module/about/about_page.dart';
 import '../module/browser/browser_page.dart';
 import '../module/character/character_controller.dart';
 import '../module/character_custom/character_custom_page.dart';
+import '../module/character_custom/detail/character_custom_detail_controller.dart';
 import '../module/feedback/feedback_page.dart';
 import '../module/keyboard_manage/keyboard_manage_page.dart';
 import '../module/login/login_page.dart';
@@ -37,6 +36,7 @@ abstract class RoutePath {
   static const browser = '/browser';
   static const keyboardManage = '/keyboardManage';
   static const characterCustom = '/characterCustom';
+  static const characterCustomDetail = '/characterCustomDetail';
 }
 
 class AppBinding extends Bindings {
@@ -53,6 +53,9 @@ class AppBinding extends Bindings {
     lazyPut(() => getIt.get<CharacterGroupContentController>());
     lazyPut(() => getIt.get<KeyboardManageController>());
     lazyPut(() => getIt.get<CharacterCustomController>());
+    lazyPut(() => getIt.get<CharacterCustomDetailController>());
+
+
   }
 
   void lazyPut<S>(InstanceBuilderCallback<S> builder) {
@@ -68,4 +71,5 @@ final generalPages = [
   GetPage(name: RoutePath.browser, page: () => BrowserPage()),
   GetPage(name: RoutePath.keyboardManage, page: () => KeyboardManagePage()),
   GetPage(name: RoutePath.characterCustom, page: () => CharacterCustomPage()),
+  GetPage(name: RoutePath.characterCustomDetail, page: () => CharacterCustomDetailPage()),
 ];

+ 48 - 0
lib/utils/styles.dart

@@ -0,0 +1,48 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import '../resource/colors.gen.dart';
+
+class Styles {
+  Styles._();
+
+  static Decoration getActivateButtonDecoration(double radius) {
+    return ShapeDecoration(
+      gradient: LinearGradient(
+
+        colors: [const Color(0xFF7D46FC), const Color(0xFFBC87FF)],
+      ),
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(radius),
+      ),
+    );
+  }
+  static Decoration getInactiveButtonDecoration(double radius) {
+    return ShapeDecoration(
+     color:  const Color(0xFF7D46FC).withAlpha(100),
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(radius),
+      ),
+    );
+  }
+
+  static TextStyle getTextStyleBlack204W400(double? sp) {
+    return TextStyle(
+      color: Colors.black.withAlpha(204),
+      fontSize: sp,
+
+      fontWeight: FontWeight.w400,
+    );
+  }
+
+  static TextStyle getTextStyleWhiteW500(double? sp) {
+    return TextStyle(
+      color: Colors.white,
+      fontSize: sp,
+      fontWeight: FontWeight.w500,
+    );
+  }
+
+
+}

+ 33 - 0
plugins/keyboard_android/.gitignore

@@ -0,0 +1,33 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+build/

+ 33 - 0
plugins/keyboard_android/.metadata

@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
+  channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+      base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+    - platform: android
+      create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+      base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+    - platform: ios
+      create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+      base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'

+ 3 - 0
plugins/keyboard_android/CHANGELOG.md

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

+ 1 - 0
plugins/keyboard_android/LICENSE

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

+ 15 - 0
plugins/keyboard_android/README.md

@@ -0,0 +1,15 @@
+# keyboard_android
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter
+[plug-in package](https://flutter.dev/to/develop-plugins),
+a specialized package that includes platform-specific implementation code for
+Android and/or iOS.
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
+

+ 4 - 0
plugins/keyboard_android/analysis_options.yaml

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

+ 9 - 0
plugins/keyboard_android/android/.gitignore

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

+ 68 - 0
plugins/keyboard_android/android/build.gradle

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

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

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

+ 32 - 0
plugins/keyboard_android/android/src/main/AndroidManifest.xml

@@ -0,0 +1,32 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.keyboard_android">
+
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+    <application>
+        <activity android:name=".InputMethodPickerActivity"
+            android:theme="@android:style/Theme.Translucent.NoTitleBar"
+            android:launchMode="singleTask"/>
+
+        <service
+            android:name="com.atmob.keyboard_android.CustomKeyboardService"
+            android:exported="true"
+            android:permission="android.permission.BIND_INPUT_METHOD"
+            android:label="追爱小键盘">
+            <intent-filter>
+                <action android:name="android.view.InputMethod" />
+            </intent-filter>
+            <meta-data
+                android:name="android.view.im"
+                android:resource="@xml/keyboard_method" />
+        </service>
+
+        <service
+            android:name="com.atmob.keyboard_android.FloatingButtonService"
+            android:exported="true"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService" />
+            </intent-filter>
+        </service>
+    </application>
+</manifest>

+ 191 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/CustomKeyboardService.kt

@@ -0,0 +1,191 @@
+package com.atmob.keyboard_android
+
+import android.content.ClipboardManager
+import android.inputmethodservice.InputMethodService
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.EditText
+import android.widget.GridLayout
+import android.widget.Toast
+import android.view.inputmethod.EditorInfo
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.FlutterEngineCache
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.EventChannel
+
+import com.atmob.keyboard_android.KeyboardAndroidPlugin
+
+class CustomKeyboardService : InputMethodService() {
+
+    private val TAG = "qqq CustomKeyboardService"
+
+    private lateinit var methodChannel: MethodChannel // 用于与 Flutter 端通信的 MethodChannel
+
+    private var keyboardView2: View? = null // 保存键盘视图
+
+    private var keyMappings: List<Pair<String, String>> = listOf()  // 存储按键映射
+
+
+    override fun onCreate() {
+        super.onCreate()
+        Log.d(TAG, "输入法服务已启动!")
+
+        val flutterEngine = FlutterEngineCache.getInstance().get("my_engine_id")
+        if (flutterEngine != null) {
+            methodChannel =
+                MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "keyboard_android")
+            Log.d(TAG, "MethodChannel 初始化成功")
+
+            // 设置 MethodChannel 的回调
+            methodChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
+                when (call.method) {
+                    "inputText" -> {
+                        val text = call.argument<String>("text")
+                        if (text != null) {
+                            inputText(text)
+                        }
+                    }
+
+                    else -> {
+                        result.notImplemented()
+                    }
+                }
+            }
+
+        } else {
+            Log.e(TAG, "FlutterEngine 未找到,MethodChannel 无法初始化")
+        }
+
+
+    }
+
+    override fun onCreateInputView(): View {
+        Log.d(TAG, "onCreateInputView!")
+        val keyboardView = layoutInflater.inflate(R.layout.keyboard_layout, null)
+        keyboardView2 = keyboardView
+
+        // 获取按键映射
+        fetchKeyMappings()
+
+        return keyboardView
+    }
+
+    override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
+        super.onStartInputView(info, restarting)
+        Log.d(TAG, "onStartInputView: 重新加载键盘数据")
+        fetchKeyMappings() // 重新获取按键映射
+    }
+
+    /**通过 KeyboardAndroidPlugin 获取按键映射**/
+    private fun fetchKeyMappings() {
+        // 通过 methodChannel 获取按键映射
+        methodChannel.invokeMethod("getKeyMappings", null, object : MethodChannel.Result {
+            override fun success(result: Any?) {
+                // 如果获取成功,处理键映射
+                if (result is List<*>) {
+                    keyMappings = result.filterIsInstance<Map<String, String>>().map {
+                        it["label"]!! to it["method"]!!
+                    }
+                    Log.d(TAG, "按键映射获取成功: $keyMappings")
+                    setupKeyboardButtons(keyboardView2!!)
+                }
+            }
+
+            override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
+                Log.e(TAG, "获取按键映射失败: $errorMessage")
+            }
+
+            override fun notImplemented() {
+                Log.e(TAG, "Flutter 端未实现 getKeyMappings 方法")
+            }
+        })
+    }
+
+
+    /**
+     * 设置键盘按钮
+     * @param keyboardView 键盘视图
+     * @param keyMappings 按键映射
+     *
+     */
+    private fun setupKeyboardButtons(keyboardView: View) {
+        Log.d(TAG, "当前键映射: $keyMappings") // 打印映射
+        val editText = keyboardView.findViewById<EditText>(R.id.keyboard_input)
+        val btnPaste = keyboardView.findViewById<Button>(R.id.btn_paste)
+        val btnDelete = keyboardView.findViewById<Button>(R.id.btn_delete)
+        val btnClear = keyboardView.findViewById<Button>(R.id.btn_clear)
+        val btnSend = keyboardView.findViewById<Button>(R.id.btn_send)
+        val numberPad = keyboardView.findViewById<GridLayout>(R.id.number_pad)
+
+        editText.setOnTouchListener { _, _ -> true } // 禁止触摸输入
+        numberPad.removeAllViews() // 清空,避免重复创建
+        Log.d(TAG, "setupKeyboardButtons: 添加 ${keyMappings.size} 个按钮")
+        keyMappings.forEach { (label, methodName) ->
+            val button = Button(this).apply {
+                text = label
+                layoutParams = GridLayout.LayoutParams()
+                setOnClickListener {
+
+                    sendDynamicTextRequest(methodName, editText.text.toString()) // **每次点击时动态获取数据**
+                }
+            }
+            numberPad.addView(button)
+        }
+
+        btnPaste.setOnClickListener { pasteText(editText) }
+        btnDelete.setOnClickListener { currentInputConnection.deleteSurroundingText(1, 0) }
+        btnClear.setOnClickListener {
+            currentInputConnection.deleteSurroundingText(
+                Int.MAX_VALUE,
+                Int.MAX_VALUE
+            )
+        }
+        btnSend.setOnClickListener { currentInputConnection.performEditorAction(android.view.inputmethod.EditorInfo.IME_ACTION_SEND) }
+    }
+
+
+    /**
+     * **动态调用 Flutter 获取文本**
+     * @param methodName 方法名
+     * @param currentContent 当前输入连接
+     */
+    private fun sendDynamicTextRequest(methodName: String, currentContent: String) {
+        methodChannel.invokeMethod(
+            "sendDynamicTextRequest",
+            mapOf("method" to methodName, "currentContent" to currentContent),
+            object : MethodChannel.Result {
+                override fun success(result: Any?) {
+                    Log.d(TAG, "sendDynamicTextRequest 请求已发送")
+                }
+
+                override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
+                    Log.e(TAG, "获取动态文本失败: $errorMessage")
+                }
+
+                override fun notImplemented() {
+                    Log.e(TAG, "Flutter 端未实现 sendDynamicTextRequest 方法")
+                }
+            })
+    }
+
+
+    //    剪贴板粘贴
+    private fun pasteText(editText: EditText) {
+        val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+        val clipData = clipboard.primaryClip
+        if (clipData != null && clipData.itemCount > 0) {
+            editText.setText(clipData.getItemAt(0).text.toString())
+        } else {
+            Toast.makeText(this, "剪贴板为空", Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    public fun inputText(text: String) {
+
+        Log.d(TAG, "inputText: $text")
+        currentInputConnection.commitText(text, 1)
+    }
+
+}

+ 147 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/FloatingButtonService.kt

@@ -0,0 +1,147 @@
+package com.atmob.keyboard_android
+
+import android.annotation.SuppressLint
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.PixelFormat
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.view.inputmethod.InputMethodManager
+import android.widget.ImageView
+
+import android.app.ActivityOptions
+import android.app.Activity
+
+class FloatingButtonService : Service() {
+    private val TAG = "qqq FloatingButtonService"
+    private lateinit var windowManager: WindowManager
+    private lateinit var floatingView: View
+    private lateinit var layoutParams: WindowManager.LayoutParams
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onCreate() {
+        super.onCreate()
+        Log.d(TAG, "onCreate: ")
+        // 初始化 WindowManager
+        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
+
+        // 加载悬浮窗布局
+        floatingView = LayoutInflater.from(this).inflate(R.layout.floating_button_layout, null)
+
+        // 悬浮窗参数
+        layoutParams = WindowManager.LayoutParams(
+            WindowManager.LayoutParams.WRAP_CONTENT,
+            WindowManager.LayoutParams.WRAP_CONTENT,
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+            else
+                WindowManager.LayoutParams.TYPE_PHONE,
+            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+
+
+                    PixelFormat.TRANSLUCENT
+        )
+        layoutParams.gravity = Gravity.TOP or Gravity.START
+
+        val screenWidth = resources.displayMetrics.widthPixels
+        val screenHeight = resources.displayMetrics.heightPixels
+        layoutParams.x = screenWidth - 200 // 初始位置在屏幕右侧
+        layoutParams.y = screenHeight / 2  // 初始位置在屏幕中间
+
+
+        // 拖动逻辑(带吸附效果)
+        val floatingButton = floatingView.findViewById<ImageView>(R.id.floating_button)
+        floatingButton.setOnTouchListener(object : View.OnTouchListener {
+            private var initialX = 0
+            private var initialY = 0
+            private var touchX = 0f
+            private var touchY = 0f
+            private var isClick = false
+
+            override fun onTouch(v: View, event: MotionEvent): Boolean {
+                when (event.action) {
+                    MotionEvent.ACTION_DOWN -> {
+                        initialX = layoutParams.x
+                        initialY = layoutParams.y
+                        touchX = event.rawX
+                        touchY = event.rawY
+                        isClick = true // 认为是点击事件,后续会判断是否有拖动
+                        return false
+                    }
+
+                    MotionEvent.ACTION_MOVE -> {
+                        val deltaX = (event.rawX - touchX).toInt()
+                        val deltaY = (event.rawY - touchY).toInt()
+                        if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10) {
+                            isClick = false // 发生拖动,不是点击
+                        }
+                        layoutParams.x = initialX + deltaX
+                        layoutParams.y = initialY + deltaY
+                        windowManager.updateViewLayout(floatingView, layoutParams)
+                        return true
+                    }
+
+                    MotionEvent.ACTION_UP -> {
+                        if (isClick) {
+                            v.performClick() // 触发点击事件
+                        } else {
+                            snapToEdge()
+                        }
+                        return true
+                    }
+                }
+                return false
+            }
+        })
+
+        // **点击按钮 -> 弹出输入法选择框**
+        floatingButton.setOnClickListener {
+            showInputMethodPicker()
+        }
+        // 添加悬浮窗到屏幕
+        windowManager.addView(floatingView, layoutParams)
+    }
+
+    /**
+     * 显示输入法选择框
+     */
+    private fun showInputMethodPicker() {
+        Log.d(TAG, "showInputMethodPicker: ")
+        val intent = Intent(this, InputMethodPickerActivity::class.java)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+
+        val options = ActivityOptions.makeCustomAnimation(this, 0, 0)
+        startActivity(intent, options.toBundle())
+    }
+
+    /**
+     * 吸附到最近的屏幕边缘
+     */
+    private fun snapToEdge() {
+        val screenWidth = resources.displayMetrics.widthPixels
+        val middle = screenWidth / 2
+        layoutParams.x = if (layoutParams.x < middle) 0 else screenWidth - floatingView.width
+        windowManager.updateViewLayout(floatingView, layoutParams)
+    }
+
+
+    override fun onDestroy() {
+        super.onDestroy()
+        windowManager.removeView(floatingView)
+    }
+
+    override fun onBind(intent: Intent?): IBinder? {
+        return null
+    }
+
+
+}

+ 61 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/InputMethodPickerActivity.kt

@@ -0,0 +1,61 @@
+package  com.atmob.keyboard_android
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.inputmethod.InputMethodManager
+
+class InputMethodPickerActivity : Activity() {
+    private var first = true
+    private val TAG = "qqq InputMethodPickerActivity"
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Log.d(TAG, "onCreate")
+
+        // 设置无动画启动
+        overridePendingTransition(0, 0)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Log.d(TAG, "onResume")
+    }
+
+    override fun onWindowFocusChanged(hasFocus: Boolean) {
+        super.onWindowFocusChanged(hasFocus)
+        Log.d(TAG, "onWindowFocusChanged focus=$hasFocus, first=$first")
+
+        if (!hasFocus) return
+
+        if (first) {
+            first = false
+            //为了确保 Activity 完全进入前台后再弹出输入法选择器
+            Handler(Looper.getMainLooper()).postDelayed({
+                Log.d(TAG, "showInputMethodPicker triggered")
+                showInputMethodPicker()
+            }, 100)
+        } else {
+            // 第二次获得焦点代表输入法弹窗消失
+            Log.d(TAG, "InputMethodPicker dismissed, finishing activity")
+            finish()
+        }
+    }
+
+    private fun showInputMethodPicker() {
+        val imeManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+        imeManager.showInputMethodPicker()
+    }
+
+
+
+
+    override fun finish() {
+        // 关闭时也不使用动画
+        overridePendingTransition(0, 0)
+        super.finish()
+    }
+}

+ 130 - 0
plugins/keyboard_android/android/src/main/kotlin/com/atmob/keyboard_android/KeyboardAndroidPlugin.kt

@@ -0,0 +1,130 @@
+package com.atmob.keyboard_android
+
+import android.content.Context
+import android.content.Context.INPUT_METHOD_SERVICE
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.util.Log
+import android.view.inputmethod.InputMethodManager
+import android.view.inputmethod.InputConnection
+
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+
+
+/** KeyboardAndroidPlugin */
+class KeyboardAndroidPlugin : FlutterPlugin, MethodCallHandler {
+
+    private lateinit var channel: MethodChannel
+
+    private lateinit var context: Context
+    private val TAG = "qqq KeyboardAndroidPlugin"
+
+
+    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+        context = flutterPluginBinding.applicationContext
+        channel = MethodChannel(flutterPluginBinding.binaryMessenger, "keyboard_android")
+        channel.setMethodCallHandler(this)
+        Log.d(TAG, "Plugin attached to engine")
+    }
+
+    override fun onMethodCall(call: MethodCall, result: Result) {
+        Log.d(TAG, "Method called: ${call.method}")
+        when (call.method) {
+            "getPlatformVersion" -> {
+                result.success("Android ${Build.VERSION.RELEASE}")
+            }
+
+            "enableFloatingWindow" -> {
+                val enable = call.argument<Boolean>("enable") ?: false
+                enableFloatingWindow(enable)
+                result.success(null)
+            }
+
+            "openInputMethodSettings" -> {
+                openInputMethodSettings()
+                result.success(null)
+            }
+
+            "isTargetKeyboardEnabled" -> {
+                result.success(isTargetKeyboardEnabled())
+            }
+
+            else -> {
+                result.notImplemented()
+            }
+        }
+    }
+
+
+    private fun enableFloatingWindow(enable: Boolean) {
+
+        if (enable) {
+            startFloatingWindowService()
+        } else {
+            closeFloatingWindowService()
+        }
+
+
+    }
+
+
+    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+        channel.setMethodCallHandler(null)
+    }
+
+    // **新增方法:返回 MethodChannel**
+    fun getMethodChannel(): MethodChannel {
+        return channel
+    }
+
+    //    关闭悬浮窗服务
+    private fun closeFloatingWindowService() {
+        Log.d(TAG, "closeFloatingWindowService: ")
+        val serviceIntent = Intent(context, FloatingButtonService::class.java)
+        context.stopService(serviceIntent)
+    }
+
+    //    打开悬浮窗服务
+    private fun startFloatingWindowService() {
+        Log.d(TAG, "startFloatingWindowService: ")
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(context)) {
+            val intent = Intent(
+                Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+                Uri.parse("package:${context.packageName}")
+            )
+            // **重要:在非 Activity 里启动 Activity 需要添加 FLAG_ACTIVITY_NEW_TASK**
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            context.startActivity(intent)
+
+        } else {
+
+            val serviceIntent = Intent(context, FloatingButtonService::class.java)
+            context.startService(serviceIntent)
+        }
+    }
+
+    //    打开输入法设置
+    private fun openInputMethodSettings() {
+        Log.d(TAG, "openInputMethodSettings: ")
+        val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        context.startActivity(intent)
+    }
+
+
+    //    判断指定的输入法是否启用
+    private fun isTargetKeyboardEnabled(): Boolean {
+        Log.d(TAG, "isTargetKeyboardEnabled: ")
+        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+        val enabledInputMethods = imm.enabledInputMethodList
+        return enabledInputMethods.any { it.packageName == context.packageName }
+    }
+
+}

+ 14 - 0
plugins/keyboard_android/android/src/main/res/layout/floating_button_layout.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content">
+
+    <ImageView
+        android:id="@+id/floating_button"
+        android:layout_width="60dp"
+        android:layout_height="60dp"
+        android:src="@android:drawable/ic_menu_manage"
+        android:background="@android:color/holo_blue_light"
+        android:scaleType="centerInside"
+        android:padding="10dp"/>
+</FrameLayout>

+ 57 - 0
plugins/keyboard_android/android/src/main/res/layout/keyboard_layout.xml

@@ -0,0 +1,57 @@
+<?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="@android:color/holo_green_light"
+    android:orientation="vertical"
+    android:padding="5dp">
+
+    <!-- 输入框 -->
+    <EditText
+        android:id="@+id/keyboard_input"
+        android:layout_width="match_parent"
+        android:layout_height="50dp"
+        android:cursorVisible="false"
+        android:focusable="false"
+        android:hint="复制过来的文本"
+        android:inputType="none"
+        android:longClickable="true"
+        android:textSize="18sp" />
+
+    <!-- 数字键盘 + 操作按钮 -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:weightSum="4">
+
+        <GridLayout
+            android:id="@+id/number_pad"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="3"
+            android:columnCount="3"
+            android:rowCount="4"
+            android:padding="5dp">
+
+            <!-- 这里原有的静态按钮定义已全部删除 -->
+
+        </GridLayout>
+
+        <!-- 操作按钮 -->
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <Button android:id="@+id/btn_paste" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="粘贴"/>
+            <Button android:id="@+id/btn_delete" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="删除"/>
+            <Button android:id="@+id/btn_clear" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="清空"/>
+            <Button android:id="@+id/btn_send" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="发送"/>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 4 - 0
plugins/keyboard_android/android/src/main/res/xml/keyboard_method.xml

@@ -0,0 +1,4 @@
+<input-method xmlns:android="http://schemas.android.com/apk/res/android"
+    android:settingsActivity=".KeyboardSettingsActivity"
+    android:isDefault="true"
+    />

+ 27 - 0
plugins/keyboard_android/android/src/test/kotlin/com/atmob/keyboard_android/KeyboardAndroidPluginTest.kt

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

+ 38 - 0
plugins/keyboard_android/ios/.gitignore

@@ -0,0 +1,38 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/ephemeral/
+/Flutter/flutter_export_environment.sh

+ 0 - 0
plugins/keyboard_android/ios/Assets/.gitkeep


+ 19 - 0
plugins/keyboard_android/ios/Classes/KeyboardAndroidPlugin.swift

@@ -0,0 +1,19 @@
+import Flutter
+import UIKit
+
+public class KeyboardAndroidPlugin: NSObject, FlutterPlugin {
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let channel = FlutterMethodChannel(name: "keyboard_android", binaryMessenger: registrar.messenger())
+    let instance = KeyboardAndroidPlugin()
+    registrar.addMethodCallDelegate(instance, channel: channel)
+  }
+
+  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+    switch call.method {
+    case "getPlatformVersion":
+      result("iOS " + UIDevice.current.systemVersion)
+    default:
+      result(FlutterMethodNotImplemented)
+    }
+  }
+}

+ 14 - 0
plugins/keyboard_android/ios/Resources/PrivacyInfo.xcprivacy

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSPrivacyTrackingDomains</key>
+	<array/>
+	<key>NSPrivacyAccessedAPITypes</key>
+	<array/>
+	<key>NSPrivacyCollectedDataTypes</key>
+	<array/>
+	<key>NSPrivacyTracking</key>
+	<false/>
+</dict>
+</plist>

+ 29 - 0
plugins/keyboard_android/ios/keyboard_android.podspec

@@ -0,0 +1,29 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint keyboard_android.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'keyboard_android'
+  s.version          = '0.0.1'
+  s.summary          = 'A new Flutter project.'
+  s.description      = <<-DESC
+A new Flutter 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.dependency 'Flutter'
+  s.platform = :ios, '12.0'
+
+  # Flutter.framework does not contain a i386 slice.
+  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+  s.swift_version = '5.0'
+
+  # If your plugin requires a privacy manifest, for example if it uses any
+  # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
+  # plugin's privacy impact, and then uncomment this line. For more information,
+  # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
+  # s.resource_bundles = {'keyboard_android_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+end

+ 36 - 0
plugins/keyboard_android/lib/keyboard_android.dart

@@ -0,0 +1,36 @@
+import 'keyboard_android_platform_interface.dart';
+
+class KeyboardAndroid {
+  Future<String?> getPlatformVersion() {
+    return KeyboardAndroidPlatform.instance.getPlatformVersion();
+  }
+
+  static Future<void> enableFloatingWindow(bool enable) async {
+    return KeyboardAndroidPlatform.instance.enableFloatingWindow(enable);
+  }
+
+  static Future<void> openInputMethodSettings() async {
+    return KeyboardAndroidPlatform.instance.openInputMethodSettings();
+  }
+
+  static Future<bool> isTargetKeyboardEnabled() async {
+    return KeyboardAndroidPlatform.instance.isTargetKeyboardEnabled();
+  }
+
+
+
+  // /// 获取键映射
+  // static Future<List<Map<String, String>>> getKeyMappings() {
+  //   return KeyboardAndroidPlatform.instance.getKeyMappings();
+  // }
+  //
+  // /// 设置键映射
+  // static Future<bool> setKeyMappings(List<Map<String, String>> mappings) {
+  //   return KeyboardAndroidPlatform.instance.setKeyMappings(mappings);
+  // }
+  //
+  // /// 通过方法名获取动态文本
+  // static Future<String?> getDynamicText(String method) {
+  //   return KeyboardAndroidPlatform.instance.getDynamicText(method);
+  // }
+}

+ 70 - 0
plugins/keyboard_android/lib/keyboard_android_method_channel.dart

@@ -0,0 +1,70 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'keyboard_android_platform_interface.dart';
+
+/// An implementation of [KeyboardAndroidPlatform] that uses method channels.
+class MethodChannelKeyboardAndroid extends KeyboardAndroidPlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  final methodChannel = const MethodChannel('keyboard_android');
+
+  @override
+  Future<String?> getPlatformVersion() async {
+    final version = await methodChannel.invokeMethod<String>(
+      'getPlatformVersion',
+    );
+    return version;
+  }
+
+  /// 打开悬浮窗
+  @override
+  Future<void> enableFloatingWindow(bool enable) async {
+    await methodChannel.invokeMethod('enableFloatingWindow', {
+      'enable': enable,
+    });
+  }
+
+  /// 打开输入法设置
+  @override
+  Future<void> openInputMethodSettings() async {
+    await methodChannel.invokeMethod('openInputMethodSettings');
+  }
+
+  /// 检查目标键盘是否启用
+  @override
+  Future<bool> isTargetKeyboardEnabled() async {
+    return await methodChannel.invokeMethod<bool>('isTargetKeyboardEnabled') ??
+        false;
+  }
+
+
+  // /// 获取键映射
+  // @override
+  // Future<List<Map<String, String>>> getKeyMappings() async {
+  //   final List<dynamic> mappings =
+  //       await methodChannel.invokeMethod<List<dynamic>>('getKeyMappings') ?? [];
+  //   return mappings.map<Map<String, String>>((dynamic item) {
+  //     final map = item as Map<dynamic, dynamic>;
+  //     return map.map<String, String>(
+  //           (key, value) => MapEntry(key.toString(), value.toString()),
+  //     );
+  //   }).toList();
+  // }
+  //
+  // /// 设置键映射
+  // @override
+  // Future<bool> setKeyMappings(List<Map<String, String>> mappings) async {
+  //   return await methodChannel.invokeMethod('setKeyMappings', {
+  //     'mappings': mappings,
+  //   });
+  // }
+  //
+  // /// 通过方法名获取动态文本
+  // @override
+  // Future<String?> getDynamicText(String method) async {
+  //   return await methodChannel.invokeMethod('getDynamicText', {
+  //     'method': method,
+  //   });
+  // }
+}

+ 66 - 0
plugins/keyboard_android/lib/keyboard_android_platform_interface.dart

@@ -0,0 +1,66 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'keyboard_android_method_channel.dart';
+
+abstract class KeyboardAndroidPlatform extends PlatformInterface {
+  /// Constructs a KeyboardAndroidPlatform.
+  KeyboardAndroidPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static KeyboardAndroidPlatform _instance = MethodChannelKeyboardAndroid();
+
+  /// The default instance of [KeyboardAndroidPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelKeyboardAndroid].
+  static KeyboardAndroidPlatform get instance => _instance;
+
+  /// Platform-specific implementations should set this with their own
+  /// platform-specific class that extends [KeyboardAndroidPlatform] when
+  /// they register themselves.
+  static set instance(KeyboardAndroidPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  Future<String?> getPlatformVersion() {
+    throw UnimplementedError('platformVersion() has not been implemented.');
+  }
+
+  Future<void> enableFloatingWindow(bool enable) {
+    throw UnimplementedError(
+      ' enableFloatingWindow(bool enable) has not been implemented.',
+    );
+  }
+
+  Future<void> openInputMethodSettings() {
+    throw UnimplementedError(
+      'openInputMethodSettings() has not been implemented.',
+    );
+  }
+
+
+  Future<bool> isTargetKeyboardEnabled() {
+    throw UnimplementedError(
+      'isTargetKeyboardEnabled() has not been implemented.',
+    );
+  }
+
+
+
+
+  // /// 获取键映射(需要在 `MethodChannelKeyboardAndroid` 实现)
+  // Future<List<Map<String, String>>> getKeyMappings() {
+  //   throw UnimplementedError('getKeyMappings() has not been implemented.');
+  // }
+  //
+  // /// 设置键映射(需要在 `MethodChannelKeyboardAndroid` 实现)
+  // Future<bool> setKeyMappings(List<Map<String, String>> mappings) {
+  //   throw UnimplementedError('setKeyMappings() has not been implemented.');
+  // }
+  //
+  // /// 通过方法名获取动态文本
+  // Future<String?> getDynamicText(String method) {
+  //   throw UnimplementedError('getDynamicText() has not been implemented.');
+  // }
+}

+ 72 - 0
plugins/keyboard_android/pubspec.yaml

@@ -0,0 +1,72 @@
+name: keyboard_android
+description: "A new Flutter project."
+version: 0.0.1
+homepage:
+
+environment:
+  sdk: ^3.7.0
+  flutter: '>=3.3.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.2
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^5.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+  # This section identifies this Flutter project as a plugin project.
+  # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
+  # which should be registered in the plugin registry. This is required for
+  # using method channels.
+  # The Android 'package' specifies package in which the registered class is.
+  # This is required for using method channels on Android.
+  # The 'ffiPlugin' specifies that native code should be built and bundled.
+  # This is required for using `dart:ffi`.
+  # All these are used by the tooling to maintain consistency when
+  # adding or updating assets for this project.
+  plugin:
+    platforms:
+      android:
+        package: com.atmob.keyboard_android
+        pluginClass: KeyboardAndroidPlugin
+      ios:
+        pluginClass: KeyboardAndroidPlugin
+
+  # To add assets to your plugin package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/to/asset-from-package
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/to/resolution-aware-images
+
+  # To add custom fonts to your plugin package, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts in packages, see
+  # https://flutter.dev/to/font-from-package

+ 27 - 0
plugins/keyboard_android/test/keyboard_android_method_channel_test.dart

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

+ 29 - 0
plugins/keyboard_android/test/keyboard_android_test.dart

@@ -0,0 +1,29 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:keyboard_android/keyboard_android.dart';
+import 'package:keyboard_android/keyboard_android_platform_interface.dart';
+import 'package:keyboard_android/keyboard_android_method_channel.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+class MockKeyboardAndroidPlatform
+    with MockPlatformInterfaceMixin
+    implements KeyboardAndroidPlatform {
+
+  @override
+  Future<String?> getPlatformVersion() => Future.value('42');
+}
+
+void main() {
+  final KeyboardAndroidPlatform initialPlatform = KeyboardAndroidPlatform.instance;
+
+  test('$MethodChannelKeyboardAndroid is the default instance', () {
+    expect(initialPlatform, isInstanceOf<MethodChannelKeyboardAndroid>());
+  });
+
+  test('getPlatformVersion', () async {
+    KeyboardAndroid keyboardAndroidPlugin = KeyboardAndroid();
+    MockKeyboardAndroidPlatform fakePlatform = MockKeyboardAndroidPlatform();
+    KeyboardAndroidPlatform.instance = fakePlatform;
+
+    expect(await keyboardAndroidPlugin.getPlatformVersion(), '42');
+  });
+}

+ 2 - 2
pubspec.lock

@@ -680,8 +680,8 @@ packages:
   keyboard_android:
     dependency: "direct main"
     description:
-      path: "D:\\flutterProject\\keyboard_android"
-      relative: false
+      path: "plugins/keyboard_android"
+      relative: true
     source: path
     version: "0.0.1"
   leak_tracker:

+ 1 - 1
pubspec.yaml

@@ -85,7 +85,7 @@ dependencies:
 #      ref: v0.0.1
 
   keyboard_android:
-    path: D:/flutterProject/keyboard_android
+    path: plugins/keyboard_android
 
 
   cupertino_icons: ^1.0.8