Browse Source

Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app

# Conflicts:
#	lib/controller/message/chat_controller.dart
ios
Jolie 4 months ago
parent
commit
a615277613
23 changed files with 1362 additions and 67 deletions
  1. 2
      .gitignore
  2. 9
      android/app/build.gradle.kts
  3. 33
      lib/controller/global.dart
  4. 41
      lib/controller/message/chat_controller.dart
  5. 6
      lib/controller/message/voice_player_manager.dart
  6. 131
      lib/controller/mine/auth_controller.dart
  7. 3
      lib/controller/mine/mine_controller.dart
  8. 14
      lib/controller/mine/user_controller.dart
  9. 87
      lib/im/im_manager.dart
  10. 2
      lib/main.dart
  11. 31
      lib/model/mine/authentication_data.dart
  12. 4
      lib/model/mine/user_data.dart
  13. 2
      lib/network/api_urls.dart
  14. 7
      lib/network/user_api.dart
  15. 42
      lib/network/user_api.g.dart
  16. 109
      lib/pages/message/chat_page.dart
  17. 126
      lib/pages/mine/auth_center_page.dart
  18. 1
      lib/pages/mine/login_page.dart
  19. 196
      lib/pages/mine/real_name_page.dart
  20. 425
      lib/rtc/rtc_manager.dart
  21. 123
      lib/widget/message/voice_item.dart
  22. 32
      pubspec.lock
  23. 3
      pubspec.yaml

2
.gitignore

@ -7,7 +7,7 @@
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
/pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.

9
android/app/build.gradle.kts

@ -37,6 +37,15 @@ android {
signingConfig = signingConfigs.getByName("debug")
}
}
packaging {
jniLibs {
pickFirsts += listOf("lib/arm64-v8a/libaosl.so")
pickFirsts += listOf("lib/armeabi-v7a/libaosl.so")
pickFirsts += listOf("lib/x86/libaosl.so")
pickFirsts += listOf("lib/x86_64/libaosl.so")
}
}
}
flutter {

33
lib/controller/global.dart

@ -0,0 +1,33 @@
// ignore_for_file: constant_identifier_names, non_constant_identifier_names
import 'dart:io';
import '../model/mine/user_data.dart';
class GlobalData {
String? qnToken;//uec接口的Token
String? userId;//id
UserData? userData;//
bool isLogout = false;//退
void logout() {
isLogout = true;
userId = null;
qnToken = null;
userData = null;
}
static GlobalData getInstance() {
_instance ??= GlobalData._init();
return _instance!;
}
GlobalData._init() {
if(Platform.isIOS){
// xAppId = "503258978847966412";
}
}
factory GlobalData() => getInstance();
static GlobalData get instance => getInstance();
static GlobalData? _instance;
}

41
lib/controller/message/chat_controller.dart

@ -21,11 +21,20 @@ class ChatController extends GetxController {
@override
void onInit() {
super.onInit();
// IMManager便 Controller
IMManager.instance.registerChatController(this);
//
fetchUserInfo();
fetchMessages();
}
@override
void onClose() {
// ChatController
IMManager.instance.unregisterChatController(userId);
super.onClose();
}
///
Future<void> fetchUserInfo() async {
try {
@ -55,7 +64,7 @@ class ChatController extends GetxController {
//
messages.insert(0, message);
update();
//
//
_refreshConversationList();
return true;
}
@ -79,7 +88,7 @@ class ChatController extends GetxController {
//
messages.insert(0, message);
update();
//
//
_refreshConversationList();
return true;
}
@ -104,7 +113,7 @@ class ChatController extends GetxController {
//
messages.insert(0, message);
update();
//
//
_refreshConversationList();
return true;
}
@ -123,7 +132,7 @@ class ChatController extends GetxController {
print('🎬 [ChatController] 准备发送视频消息');
print('视频路径: $filePath');
print('视频时长: $duration');
final message = await IMManager.instance.sendVideoMessage(
filePath,
userId,
@ -135,7 +144,7 @@ class ChatController extends GetxController {
//
messages.insert(0, message);
update();
//
//
_refreshConversationList();
return true;
}
@ -198,14 +207,32 @@ class ChatController extends GetxController {
}
}
///
void addReceivedMessage(EMMessage message) {
//
if (!messages.any((msg) => msg.msgId == message.msgId)) {
//
messages.insert(0, message);
update();
//
_refreshConversationList();
if (Get.isLogEnable) {
Get.log('收到新消息并添加到列表: ${message.msgId}');
}
}
}
///
void _refreshConversationList() {
try {
//
// ConversationController
if (Get.isRegistered<ConversationController>()) {
Get.find<ConversationController>().refreshConversations();
final conversationController = Get.find<ConversationController>();
conversationController.refreshConversations();
}
} catch (e) {
// ConversationController
if (Get.isLogEnable) {
Get.log('刷新会话列表失败: $e');
}

6
lib/controller/message/voice_player_manager.dart

@ -49,7 +49,11 @@ class VoicePlayerManager extends GetxController {
//
_currentPlayingId = audioId;
currentPlayingId.value = audioId;
await _audioPlayer.play(DeviceFileSource(filePath));
if(filePath.startsWith('https://')){
await _audioPlayer.play(UrlSource(filePath));
}else{
await _audioPlayer.play(DeviceFileSource(filePath));
}
} catch (e) {
print('播放音频失败: $e');
_currentPlayingId = null;

131
lib/controller/mine/auth_controller.dart

@ -0,0 +1,131 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../network/user_api.dart';
import '../global.dart';
class AuthController extends GetxController {
final isLoading = false.obs;
final List<AuthCard> dataList = [];
//
final isLoggingIn = false.obs;
final name = ''.obs;
final idcard = ''.obs;
final agree = false.obs;
// GetX依赖注入中获取UserApi实例
late UserApi _userApi;
@override
void onInit() {
super.onInit();
// UserApi
_userApi = Get.find<UserApi>();
_loadInitialData();
}
//
Future<void> _loadInitialData() async {
try {
isLoading.value = true;
late bool realAuth = false;
if(GlobalData().userData?.realAuth != null){
realAuth = GlobalData().userData!.realAuth!;
}
dataList.assignAll([
AuthCard( title: '手机绑定', desc: '防止账号丢失', index: 1, authed: true),
AuthCard( title: '真实头像', desc: '提高交友成功率', index: 2, authed: false),
AuthCard( title: '实名认证', desc: '提高交友成功率', index: 3, authed: false),
]);
//
// final response = await _userApi.login({});
//
// if (response.data.isSuccess) {
//
// }
} catch (e) {
SmartDialog.showToast('网络请求失败,请检查网络连接');
} finally {
isLoading.value = false;
}
}
bool validateChineseID(String id) {
if (id.length != 18) return false;
//
final coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
//
final checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
int sum = 0;
try {
for (int i = 0; i < 17; i++) {
int digit = int.parse(id[i]);
sum += digit * coefficients[i];
}
} catch (e) {
return false; //
}
int remainder = sum % 11;
String checkCode = checkCodeMap[remainder];
return id[17].toUpperCase() == checkCode;
}
Future<void> startAuthing() async {
if (name.value.isEmpty) {
SmartDialog.showToast('请输入姓名');
return;
}
if (idcard.value.isEmpty) {
SmartDialog.showToast('请输入身份证号');
return;
}
if (!validateChineseID(idcard.value)) {
SmartDialog.showToast('请输入正确的身份证号');
return;
}
if (!agree.value) {
SmartDialog.showToast('请同意用户认证协议');
return;
}
try {
//
final param = {
'miId': GlobalData().userData?.id,
'authenticationCode': 0,
'value': '${name.value},${idcard.value}',
};
final response = await _userApi.saveCertificationAudit(param);
//
if (response.data.isSuccess) {
GlobalData().userData?.realAuth = true;
SmartDialog.showToast('认证成功');
Get.back(result: {'index': 3});
} else {
SmartDialog.showToast(response.data.message);
}
} catch (e) {
SmartDialog.showToast('网络请求失败,请检查网络连接');
} finally {
}
}
}
class AuthCard {
final String title;
final String desc;
final int index;
final bool authed;
AuthCard({
required this.desc,
required this.title,
required this.index,
required this.authed,
});
}

3
lib/controller/mine/mine_controller.dart

@ -1,4 +1,5 @@
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/mine/auth_center_page.dart';
import 'package:dating_touchme_app/pages/mine/my_wallet_page.dart';
import 'package:dating_touchme_app/pages/mine/rose_page.dart';
import 'package:get/get.dart';
@ -18,7 +19,7 @@ class MineController extends GetxController {
{"icon": Assets.imagesRose, "title": "我的玫瑰", "subTitle": "新人限时福利", "path": () => RosePage()},
{"icon": Assets.imagesWallet, "title": "我的钱包", "subTitle": "提现无门槛", "path": () => MyWalletPage()},
{"icon": Assets.imagesShop, "title": "商城中心", "subTitle": "不定期更新商品", "path": () => Null},
{"icon": Assets.imagesCert, "title": "认证中心", "subTitle": "未认证", "path": () => Null},
{"icon": Assets.imagesCert, "title": "认证中心", "subTitle": "未认证", "path": () => AuthCenterPage()},
].obs;
List<Map> settingList = [

14
lib/controller/mine/user_controller.dart

@ -2,9 +2,12 @@ import 'package:dating_touchme_app/im/im_manager.dart';
import 'package:dating_touchme_app/oss/oss_manager.dart';
import 'package:get/get.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../../model/mine/authentication_data.dart';
import '../../model/mine/user_data.dart';
import '../../network/user_api.dart';
import '../../pages/mine/user_info_page.dart';
import '../../pages/main/main_page.dart';
import '../global.dart';
class UserController extends GetxController {
@ -72,8 +75,15 @@ class UserController extends GetxController {
final response = await _userApi.getMarriageInformationDetail();
if (response.data.isSuccess) {
// data是否为null或者是空对象
final information = response.data.data;
if (information == null || information.id.isEmpty || information.genderCode.isNaN || information.birthYear == null) {
final information = response.data.data!;
if (information.id.isNotEmpty) {
final result = await _userApi.getCertificationList(information.id);
List<AuthenticationData> list = result.data.data!;
final record = list.firstWhere((item) => item.authenticationCode == 0);
information.realAuth = record.status == 1;
}
GlobalData().userData = information;
if (information.id.isEmpty || information.genderCode.isNaN || information.birthYear == null) {
//
SmartDialog.showToast('转到完善信息');
//

87
lib/im/im_manager.dart

@ -1,5 +1,8 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../controller/message/conversation_controller.dart';
import '../controller/message/chat_controller.dart';
// IM管理器实现使SDK类型和方法
class IMManager {
@ -12,6 +15,9 @@ class IMManager {
bool _isInitialized = false;
// ChatController key userId
final Map<String, ChatController> _activeChatControllers = {};
IMManager._internal() {
print('IMManager instance created');
}
@ -71,6 +77,12 @@ class IMManager {
"",
EMChatEventHandler(
onMessagesReceived: (messages) {
//
_refreshConversationList();
// ChatController
_notifyChatControllers(messages);
for (var msg in messages) {
switch (msg.body.type) {
case MessageType.TXT:
@ -167,16 +179,16 @@ class IMManager {
///
Future<EMMessage?> sendVoiceMessage(
String filePath,
String toChatUsername,
int duration
) async {
String filePath,
String toChatUsername,
int duration,
) async {
try {
//
final message = EMMessage.createVoiceSendMessage(
targetId: toChatUsername,
filePath: filePath,
duration: duration
duration: duration,
);
//
@ -223,14 +235,14 @@ class IMManager {
print('视频路径: $videoPath');
print('接收用户: $toChatUsername');
print('视频时长: $duration');
//
final message = EMMessage.createVideoSendMessage(
targetId: toChatUsername,
filePath: videoPath,
duration: duration,
);
print('消息创建成功,消息类型: ${message.body.type}');
print('消息体是否为视频: ${message.body is EMVideoMessageBody}');
@ -280,6 +292,67 @@ class IMManager {
return data[userId];
}
/// ChatController
void registerChatController(ChatController controller) {
_activeChatControllers[controller.userId] = controller;
if (Get.isLogEnable) {
Get.log('注册 ChatController: ${controller.userId}');
}
}
/// ChatController
void unregisterChatController(String userId) {
_activeChatControllers.remove(userId);
if (Get.isLogEnable) {
Get.log('注销 ChatController: $userId');
}
}
/// ChatController
void _notifyChatControllers(List<EMMessage> messages) {
try {
//
for (var message in messages) {
// direction == RECEIVE
if (message.direction == MessageDirection.RECEIVE) {
// IDfrom
final fromId = message.from;
if (fromId != null && fromId.isNotEmpty) {
// ChatController
final controller = _activeChatControllers[fromId];
if (controller != null) {
controller.addReceivedMessage(message);
if (Get.isLogEnable) {
Get.log('通知 ChatController 更新消息: $fromId');
}
}
}
}
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('通知 ChatController 更新消息列表失败: $e');
}
}
}
///
void _refreshConversationList() {
try {
// ConversationController
if (Get.isRegistered<ConversationController>()) {
final conversationController = Get.find<ConversationController>();
conversationController.refreshConversations();
}
} catch (e) {
// ConversationController
if (Get.isLogEnable) {
Get.log('刷新会话列表失败: $e');
}
}
}
///
void dispose() {
try {

2
lib/main.dart

@ -6,6 +6,7 @@ import 'package:dating_touchme_app/network/network_service.dart';
import 'package:dating_touchme_app/pages/main/main_page.dart';
import 'package:dating_touchme_app/pages/mine/login_page.dart';
import 'package:dating_touchme_app/pages/mine/user_info_page.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
@ -21,6 +22,7 @@ void main() async {
// - release模式
EnvConfig.setEnvironment(Environment.dev);
RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d');
IMManager.instance.initialize('1165251016193374#demo');
//
final networkService = NetworkService();

31
lib/model/mine/authentication_data.dart

@ -0,0 +1,31 @@
class AuthenticationData {
final int? authenticationCode;
final String? authenticationName;
final String? miId;
final int? status;
AuthenticationData({
this.authenticationCode,
this.authenticationName,
this.miId,
this.status,
});
factory AuthenticationData.fromJson(Map<String, dynamic> json) {
return AuthenticationData(
authenticationCode: json['authenticationCode'] as int?,
authenticationName: json['authenticationName'] as String?,
miId: json['miId'] as String?,
status: json['status'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'authenticationCode': authenticationCode,
'authenticationName': authenticationName,
'miId': miId,
'status': status,
};
}
}

4
lib/model/mine/user_data.dart

@ -59,6 +59,7 @@ class UserData {
final String? hometownProvinceName;
final String? hometownCityCode;
final String? hometownCityName;
bool? realAuth;
UserData({
required this.id,
@ -120,6 +121,7 @@ class UserData {
this.hometownProvinceName,
this.hometownCityCode,
this.hometownCityName,
this.realAuth,
});
// JSON映射创建实例
@ -184,6 +186,7 @@ class UserData {
hometownProvinceName: json['hometownProvinceName'],
hometownCityCode: json['hometownCityCode'],
hometownCityName: json['hometownCityName'],
realAuth: json['realAuth'],
);
}
@ -249,6 +252,7 @@ class UserData {
'hometownProvinceName': hometownProvinceName,
'hometownCityCode': hometownCityCode,
'hometownCityName': hometownCityName,
'realAuth': realAuth,
};
}

2
lib/network/api_urls.dart

@ -12,7 +12,7 @@ class ApiUrls {
static const String getHxUserToken = 'dating-agency-chat-audio/user/get/hx/user/token';
static const String getApplyTempAuth = 'dating-agency-uec/get/apply-temp-auth';
static const String saveCertificationAudit = 'dating-agency-service/user/save/certification/audit';
static const String getCertificationList = '/dating-agency-service/user/get/certification/item/all/list';
//
static const String getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information';

7
lib/network/user_api.dart

@ -7,6 +7,8 @@ import 'package:dating_touchme_app/network/api_urls.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
import '../model/mine/authentication_data.dart';
part 'user_api.g.dart';
@RestApi(baseUrl: '')
@ -41,6 +43,11 @@ abstract class UserApi {
@Body() Map<String, dynamic> data,
);
@GET(ApiUrls.getCertificationList)
Future<HttpResponse<BaseResponse<List<AuthenticationData>>>> getCertificationList(
@Query('miId') String miId,
);
@GET(ApiUrls.getHxUserToken)
Future<HttpResponse<BaseResponse<String>>> getHxUserToken();

42
lib/network/user_api.g.dart

@ -220,6 +220,48 @@ class _UserApi implements UserApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<List<AuthenticationData>>>>
getCertificationList(String miId) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{r'miId': miId};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options =
_setStreamType<HttpResponse<BaseResponse<List<AuthenticationData>>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'/dating-agency-service/user/get/certification/item/all/list',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<List<AuthenticationData>> _value;
try {
_value = BaseResponse<List<AuthenticationData>>.fromJson(
_result.data!,
(json) => json is List<dynamic>
? json
.map<AuthenticationData>(
(i) =>
AuthenticationData.fromJson(i as Map<String, dynamic>),
)
.toList()
: List.empty(),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<String>>> getHxUserToken() async {
final _extra = <String, dynamic>{};

109
lib/pages/message/chat_page.dart

@ -10,15 +10,48 @@ import '../../generated/assets.dart';
import '../../../widget/message/chat_input_bar.dart';
import '../../../widget/message/message_item.dart';
class ChatPage extends StatelessWidget {
class ChatPage extends StatefulWidget {
final String userId;
const ChatPage({required this.userId, super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
late ChatController _controller;
@override
void initState() {
super.initState();
// controller
_controller = Get.put(ChatController(userId: widget.userId));
//
_scrollController.addListener(() {
if (_scrollController.hasClients &&
_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
!_isLoadingMore &&
_controller.messages.isNotEmpty) {
_loadMoreMessages();
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GetBuilder<ChatController>(
init: ChatController(userId: userId),
init: _controller,
builder: (controller) {
return WillPopScope(
onWillPop: () async {
@ -57,6 +90,7 @@ class ChatPage extends StatelessWidget {
},
behavior: HitTestBehavior.opaque,
child: ListView.builder(
controller: _scrollController,
reverse: true,
padding: EdgeInsets.all(16.w),
itemCount: controller.messages.length,
@ -79,34 +113,53 @@ class ChatPage extends StatelessWidget {
),
),
),
// 使
ChatInputBar(
onSendMessage: (message) async {
await controller.sendMessage(message);
},
onImageSelected: (imagePaths) async {
//
for (var imagePath in imagePaths) {
await controller.sendImageMessage(imagePath);
}
},
onVoiceRecorded: (filePath, seconds) async {
//
await controller.sendVoiceMessage(filePath, seconds);
},
onVideoRecorded: (filePath, duration) async {
print('🎬 [ChatPage] 收到视频录制/选择回调');
print('文件路径: $filePath');
print('时长: $duration');
// /
await controller.sendVideoMessage(filePath, duration);
},
),
],
// 使
ChatInputBar(
onSendMessage: (message) async {
await controller.sendMessage(message);
},
onImageSelected: (imagePaths) async {
//
for (var imagePath in imagePaths) {
await controller.sendImageMessage(imagePath);
}
},
onVoiceRecorded: (filePath, seconds) async {
//
await controller.sendVoiceMessage(filePath, seconds);
},
onVideoRecorded: (filePath, duration) async {
print('🎬 [ChatPage] 收到视频录制/选择回调');
print('文件路径: $filePath');
print('时长: $duration');
// /
await controller.sendVideoMessage(filePath, duration);
},
),
],
),
),
),
);
);
},
);
}
//
Future<void> _loadMoreMessages() async {
if (_isLoadingMore) return;
setState(() {
_isLoadingMore = true;
});
try {
await _controller.fetchMessages(loadMore: true);
} finally {
if (mounted) {
setState(() {
_isLoadingMore = false;
});
}
}
}
}

126
lib/pages/mine/auth_center_page.dart

@ -0,0 +1,126 @@
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/pages/mine/real_name_page.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../controller/mine/auth_controller.dart';
import 'edit_info_page.dart';
class AuthCenterPage extends StatelessWidget {
AuthCenterPage({super.key});
final AuthController controller = Get.put(AuthController());
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xffF5F5F5),
appBar: AppBar(
title: Text('认证中心', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,),
onPressed: () {
Get.back();
},
),
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CupertinoActivityIndicator(radius: 12,));
}
return ListView.builder(
padding: const EdgeInsets.only(top: 16, right: 16, left: 16),
itemCount: controller.dataList.length,
itemBuilder: (context, index) {
final record = controller.dataList[index];
return _buildListItem(record);
},
);
})
);
}
//
Widget _buildListItem(AuthCard item) {
return Container(
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.blue[100],
image: DecorationImage(
image: NetworkImage('https://picsum.photos/40/40?random=$item.index'),
fit: BoxFit.cover,
),
),
),
SizedBox(width: 12),
//
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
SizedBox(height: 2),
Text(
item.desc,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
Spacer(),
Row(
children: [
Text(
item.authed ? '已认证' : '去认证',
style: TextStyle(
fontSize: 12,
color: item.authed ? Color(0xff26C77C) : Colors.grey[500]
)
),
SizedBox(width: 4),
item.authed ? SizedBox(width: 24) : Icon(
Icons.navigate_next, // Material Icons
// size: 128.0, // #26C77C
color: Colors.grey[500]
),
],
)
],
),
).onTap(() async{
if(!item.authed){
if(item.index == 2){
Get.to(() => EditInfoPage());
} else if(item.index == 3){
final result = await Get.to(() => RealNamePage());
print(result);
}
}
});
}
}

1
lib/pages/mine/login_page.dart

@ -30,7 +30,6 @@ class LoginPage extends StatelessWidget {
child: Column(
children: [
const SizedBox(height: 150),
// Logo和标题区域
Center(
child: Column(

196
lib/pages/mine/real_name_page.dart

@ -0,0 +1,196 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import '../../controller/mine/auth_controller.dart';
class RealNamePage extends StatelessWidget {
RealNamePage({super.key});
final AuthController controller = Get.put(AuthController());
//
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xffFFFFFF),
appBar: AppBar(
title: Text('实名认证', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
centerTitle: true,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,),
onPressed: () {
Get.back();
},
),
),
body: Column(
children: [
Container(
height: 48,
decoration: BoxDecoration(color: Color(0xffE7E7E7)),
padding: const EdgeInsets.only(left: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, //
children: [
Text(
'*请填写本人实名信息',
style: TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
],
),
),
Container(
height: 56, //
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey[400]!,
width: 0.5,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, //
children: [
// - +
Container(
width: 100,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 16),
child: Text(
'姓名:',
style: TextStyle(
fontSize: 15,
color: Colors.black87,
),
),
),
SizedBox(width: 12),
// - 使Expanded填充剩余空间
Expanded(
child: Container(
alignment: Alignment.centerLeft, //
child: TextField(
decoration: InputDecoration(
hintText: '请输入姓名',
hintStyle: TextStyle(color: Colors.grey[500]),
border: InputBorder.none, //
contentPadding: EdgeInsets.zero, // padding
isDense: true, //
),
style: TextStyle(
fontSize: 15,
height: 1.2, //
),
onChanged: (value) {
controller.name.value = value;
},
),
),
),
],
),
),
// SizedBox(height: 30),
Container(
height: 56, //
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey[400]!,
width: 0.5,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, //
children: [
// - +
Container(
width: 100,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 16),
child: Text(
'身份证号:',
style: TextStyle(
fontSize: 15,
color: Colors.black87,
),
),
),
SizedBox(width: 12),
// - 使Expanded填充剩余空间
Expanded(
child: Container(
alignment: Alignment.centerLeft, //
child: TextField(
decoration: InputDecoration(
hintText: '请输入身份证号',
hintStyle: TextStyle(color: Colors.grey[500]),
border: InputBorder.none, //
contentPadding: EdgeInsets.zero, // padding
isDense: true, //
),
style: TextStyle(
fontSize: 15,
height: 1.2, //
),
onChanged: (value) {
controller.idcard.value = value;
},
),
),
),
],
),
),
SizedBox(height: 24),
//
Row(
crossAxisAlignment: CrossAxisAlignment.start, //
children: [
SizedBox(width: 8),
Obx(() => Checkbox(
value: controller.agree.value,
onChanged: (value) {
controller.agree.value = value ?? false;
},
activeColor: Color(0xff7562F9),
side: const BorderSide(color: Colors.grey),
shape: const CircleBorder(),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
)),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 300), //
child: Text(
'依据法律法规的要求,我们将收集您的真实姓名,身份证号用于实名认证,认证信息将用于直播连麦、收益提现、依据证件信息更正性别等,与账号唯一绑定,我们会使用加密方式对您的认证信息进行严格保密。',
style: TextStyle( fontSize: 13, color: Colors.grey ),
),
)
],
),
SizedBox(height: 48),
TDButton(
text: '立即认证',
width: MediaQuery.of(context).size.width - 40,
size: TDButtonSize.large,
type: TDButtonType.fill,
shape: TDButtonShape.round,
theme: TDButtonTheme.primary,
onTap: (){
controller.startAuthing();
},
),
],
),
);
}
}

425
lib/rtc/rtc_manager.dart

@ -0,0 +1,425 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
/// RTC
class RTCManager {
//
static final RTCManager _instance = RTCManager._internal();
factory RTCManager() => _instance;
// getter用于instance访问
static RTCManager get instance => _instance;
RtcEngine? _engine;
bool _isInitialized = false;
bool _isInChannel = false;
String? _currentChannelId;
int? _currentUid;
//
Function(RtcConnection connection, int elapsed)? onJoinChannelSuccess;
Function(RtcConnection connection, int remoteUid, int elapsed)? onUserJoined;
Function(
RtcConnection connection,
int remoteUid,
UserOfflineReasonType reason,
)?
onUserOffline;
Function(RtcConnection connection, RtcStats stats)? onLeaveChannel;
Function(
RtcConnection connection,
ConnectionStateType state,
ConnectionChangedReasonType reason,
)?
onConnectionStateChanged;
Function(int uid, UserInfo userInfo)? onUserInfoUpdated;
Function(RtcConnection connection, int uid, int elapsed)?
onFirstRemoteVideoDecoded;
Function(
RtcConnection connection,
VideoSourceType sourceType,
int uid,
int width,
int height,
int rotation,
)?
onVideoSizeChanged;
Function(RtcConnection connection, int uid, bool muted)? onUserMuteAudio;
Function(RtcConnection connection, int uid, bool muted)? onUserMuteVideo;
Function(RtcConnection connection)? onConnectionLost;
Function(RtcConnection connection, int code, String msg)? onError;
RTCManager._internal() {
print('RTCManager instance created');
}
/// RTC Engine
/// [appId] App ID
/// [channelProfile]
Future<bool> initialize({
required String appId,
ChannelProfileType channelProfile =
ChannelProfileType.channelProfileCommunication,
}) async {
try {
if (_isInitialized && _engine != null) {
print('RTC Engine already initialized');
return true;
}
// RTC Engine
_engine = createAgoraRtcEngine();
// RTC Engine
await _engine!.initialize(
RtcEngineContext(appId: appId, channelProfile: channelProfile),
);
//
_registerEventHandlers();
_isInitialized = true;
print('RTC Engine initialized successfully');
return true;
} catch (e) {
print('Failed to initialize RTC Engine: $e');
return false;
}
}
///
void _registerEventHandlers() {
if (_engine == null) return;
_engine!.registerEventHandler(
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
_isInChannel = true;
_currentChannelId = connection.channelId;
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms');
if (onJoinChannelSuccess != null) {
onJoinChannelSuccess!(connection, elapsed);
}
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
print('用户加入,UID:$remoteUid');
if (onUserJoined != null) {
onUserJoined!(connection, remoteUid, elapsed);
}
},
onUserOffline:
(
RtcConnection connection,
int remoteUid,
UserOfflineReasonType reason,
) {
print('用户离开,UID:$remoteUid,原因:$reason');
if (onUserOffline != null) {
onUserOffline!(connection, remoteUid, reason);
}
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
_isInChannel = false;
_currentChannelId = null;
print('离开频道,统计信息:${stats.duration}');
if (onLeaveChannel != null) {
onLeaveChannel!(connection, stats);
}
},
onConnectionStateChanged:
(
RtcConnection connection,
ConnectionStateType state,
ConnectionChangedReasonType reason,
) {
print('连接状态改变:$state,原因:$reason');
if (onConnectionStateChanged != null) {
onConnectionStateChanged!(connection, state, reason);
}
},
onUserInfoUpdated: (int uid, UserInfo userInfo) {
print('用户信息更新,UID:$uid');
if (onUserInfoUpdated != null) {
onUserInfoUpdated!(uid, userInfo);
}
},
onFirstRemoteVideoDecoded:
(
RtcConnection connection,
int uid,
int width,
int height,
int elapsed,
) {
print('首次远程视频解码,UID:$uid,分辨率:${width}x${height}');
if (onFirstRemoteVideoDecoded != null) {
onFirstRemoteVideoDecoded!(connection, uid, elapsed);
}
},
onVideoSizeChanged:
(
RtcConnection connection,
VideoSourceType sourceType,
int uid,
int width,
int height,
int rotation,
) {
print('视频尺寸改变,UID:$uid,分辨率:${width}x${height}');
if (onVideoSizeChanged != null) {
onVideoSizeChanged!(
connection,
sourceType,
uid,
width,
height,
rotation,
);
}
},
onUserMuteAudio: (RtcConnection connection, int uid, bool muted) {
print('用户静音状态改变,UID:$uid,静音:$muted');
if (onUserMuteAudio != null) {
onUserMuteAudio!(connection, uid, muted);
}
},
onUserMuteVideo: (RtcConnection connection, int uid, bool muted) {
print('用户视频状态改变,UID:$uid,关闭:$muted');
if (onUserMuteVideo != null) {
onUserMuteVideo!(connection, uid, muted);
}
},
onConnectionLost: (RtcConnection connection) {
print('连接丢失');
if (onConnectionLost != null) {
onConnectionLost!(connection);
}
},
onError: (ErrorCodeType err, String msg) {
print('RTC Engine 错误:$err,消息:$msg');
if (onError != null) {
onError!(
RtcConnection(
channelId: _currentChannelId ?? '',
localUid: _currentUid ?? 0,
),
err.value(),
msg,
);
}
},
),
);
}
///
Future<void> enableVideo() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.enableVideo();
print('视频已启用');
}
///
Future<void> disableVideo() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.disableVideo();
print('视频已禁用');
}
///
Future<void> enableAudio() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.enableAudio();
print('音频已启用');
}
///
Future<void> disableAudio() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.disableAudio();
print('音频已禁用');
}
///
Future<void> startPreview() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.startPreview();
print('本地视频预览已开启');
}
///
Future<void> stopPreview() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.stopPreview();
print('本地视频预览已停止');
}
///
/// [viewId] ID
/// [mirrorMode]
Future<void> setupLocalVideo({
required int viewId,
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary,
VideoMirrorModeType mirrorMode = VideoMirrorModeType.videoMirrorModeAuto,
}) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.setupLocalVideo(
VideoCanvas(view: viewId, sourceType: sourceType, mirrorMode: mirrorMode),
);
print('本地视频视图已设置,viewId:$viewId');
}
///
/// [uid] ID
/// [viewId] ID
Future<void> setupRemoteVideo({
required int uid,
required int viewId,
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary,
VideoMirrorModeType mirrorMode =
VideoMirrorModeType.videoMirrorModeDisabled,
}) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.setupRemoteVideo(
VideoCanvas(
uid: uid,
view: viewId,
sourceType: sourceType,
mirrorMode: mirrorMode,
),
);
print('远程视频视图已设置,UID:$uid,viewId:$viewId');
}
///
/// [token]
/// [channelId] ID
/// [uid] ID0
/// [options]
Future<void> joinChannel({
String? token,
required String channelId,
int uid = 0,
ChannelMediaOptions? options,
}) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
if (_isInChannel) {
print('已经在频道中,先离开当前频道');
await leaveChannel();
}
_currentUid = uid;
await _engine!.joinChannel(
token: token ?? '',
channelId: channelId,
uid: uid,
options: options ?? const ChannelMediaOptions(),
);
print('正在加入频道:$channelId,UID:$uid');
}
///
Future<void> leaveChannel() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
if (!_isInChannel) {
print('当前不在频道中');
return;
}
await _engine!.leaveChannel();
_currentUid = null;
print('已离开频道');
}
///
Future<void> switchCamera() async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.switchCamera();
print('摄像头已切换');
}
/// /
/// [muted] true表示静音false表示取消静音
Future<void> muteLocalAudio(bool muted) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.muteLocalAudioStream(muted);
print('本地音频${muted ? "已静音" : "已取消静音"}');
}
/// /
/// [enabled] true表示开启false表示关闭
Future<void> muteLocalVideo(bool enabled) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.muteLocalVideoStream(!enabled);
print('本地视频${enabled ? "已开启" : "已关闭"}');
}
///
/// [role]
Future<void> setClientRole({
required ClientRoleType role,
ClientRoleOptions? options,
}) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.setClientRole(role: role, options: options);
print('客户端角色已设置为:$role');
}
///
bool get isInChannel => _isInChannel;
/// ID
String? get currentChannelId => _currentChannelId;
/// ID
int? get currentUid => _currentUid;
/// RTC Engine
RtcEngine? get engine => _engine;
///
Future<void> dispose() async {
try {
if (_isInChannel) {
await leaveChannel();
}
if (_engine != null) {
await _engine!.release();
_engine = null;
}
_isInitialized = false;
_isInChannel = false;
_currentChannelId = null;
_currentUid = null;
print('RTC Engine disposed');
} catch (e) {
print('Failed to dispose RTC Engine: $e');
}
}
}

123
lib/widget/message/voice_item.dart

@ -1,3 +1,6 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:audioplayers/audioplayers.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -28,25 +31,67 @@ class VoiceItem extends StatefulWidget {
State<VoiceItem> createState() => _VoiceItemState();
}
class _VoiceItemState extends State<VoiceItem> {
class _VoiceItemState extends State<VoiceItem> with TickerProviderStateMixin {
final VoicePlayerManager _playerManager = VoicePlayerManager.instance;
late AnimationController _waveformAnimationController;
int _animationFrame = 0;
@override
void initState() {
super.initState();
//
_waveformAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000), // 2
);
//
_waveformAnimationController.addListener(() {
if (mounted && _playerManager.isPlaying(widget.messageId)) {
setState(() {
// 使
_animationFrame++;
});
}
});
//
ever(_playerManager.currentPlayingId, (audioId) {
if (mounted) {
setState(() {});
//
if (_playerManager.isPlaying(widget.messageId)) {
if (!_waveformAnimationController.isAnimating) {
_waveformAnimationController.repeat();
}
} else {
_waveformAnimationController.stop();
_waveformAnimationController.reset();
_animationFrame = 0;
}
}
});
ever(_playerManager.playerState, (state) {
if (mounted) {
setState(() {});
//
if (state == PlayerState.playing &&
_playerManager.currentPlayingId.value == widget.messageId) {
_waveformAnimationController.repeat();
} else {
_waveformAnimationController.stop();
_animationFrame = 0;
}
}
});
}
@override
void dispose() {
_waveformAnimationController.dispose();
super.dispose();
}
// /
Future<void> _handlePlayPause() async {
try {
@ -55,19 +100,37 @@ class _VoiceItemState extends State<VoiceItem> {
final localPath = widget.voiceBody.localPath;
final remotePath = widget.voiceBody.remotePath;
if (localPath.isNotEmpty) {
filePath = localPath;
} else if (remotePath != null && remotePath.isNotEmpty) {
//
filePath = remotePath;
// 使
if (remotePath != null && remotePath.isNotEmpty) {
// 使audioplayers URL
// URL
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
filePath = remotePath;
} else {
SmartDialog.showToast('音频文件不存在,请等待下载完成');
print('远程音频文件路径: $remotePath');
return;
}
} else if (localPath.isNotEmpty) {
final localFile = File(localPath);
if (await localFile.exists()) {
filePath = localPath;
} else {
SmartDialog.showToast('音频文件不存在');
print('本地音频文件不存在: $localPath');
return;
}
}
SmartDialog.showToast('来了$remotePath');
if (filePath != null && filePath.isNotEmpty) {
await _playerManager.play(widget.messageId, filePath);
} else {
SmartDialog.showToast('无法获取音频文件');
print('音频文件路径为空');
}
} catch (e) {
SmartDialog.showToast('播放失败: $e');
print('播放音频失败: $e');
}
}
@ -120,9 +183,7 @@ class _VoiceItemState extends State<VoiceItem> {
height: 20.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.isSentByMe
? Colors.white
: Colors.black,
color: widget.isSentByMe ? Colors.white : Colors.black,
),
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
@ -193,6 +254,7 @@ class _VoiceItemState extends State<VoiceItem> {
Widget _buildWaveform() {
// 20
final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20);
final isPlaying = _playerManager.isPlaying(widget.messageId);
return SizedBox(
height: 16.h,
@ -200,19 +262,44 @@ class _VoiceItemState extends State<VoiceItem> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(barCount, (index) {
//
final random = (index * 7) % 5;
final baseHeight = 6 + random * 2;
final height = (baseHeight.clamp(4, 16)).h;
double height;
Color color;
if (isPlaying) {
//
// 使
// wavePhase: (_animationFrame)(index)
//
final wavePhase = _animationFrame * 0.15 + index * 0.6;
// 使 4-16
final sinValue = math.sin(wavePhase);
final normalizedValue = (sinValue + 1) / 2; // 0-1
final baseHeight = 4 + normalizedValue * 12;
height = (baseHeight.clamp(4, 16)).h;
//
final opacity = 0.5 + normalizedValue * 0.5;
color = widget.isSentByMe
? Colors.white.withOpacity(opacity.clamp(0.5, 1.0))
: Colors.grey.withOpacity(opacity.clamp(0.5, 0.9));
} else {
//
final random = (index * 7) % 5;
final baseHeight = 6 + random * 2;
height = (baseHeight.clamp(4, 16)).h;
color = widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6);
}
return Container(
return AnimatedContainer(
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
width: 2.w,
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: widget.isSentByMe
? Colors.white.withOpacity(0.8)
: Colors.grey.withOpacity(0.6),
color: color,
borderRadius: BorderRadius.circular(1.w),
),
);

32
pubspec.lock

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "88.0.0"
agora_rtc_engine:
dependency: "direct main"
description:
name: agora_rtc_engine
sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e"
url: "https://pub.dev"
source: hosted
version: "6.5.3"
analyzer:
dependency: transitive
description:
@ -573,6 +581,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluwx:
dependency: "direct main"
description:
name: fluwx
sha256: "7e92d2000ee49c5262a88c51ea2d22b91a753d5b29df27cc264bb0a115d65373"
url: "https://pub.dev"
source: hosted
version: "5.7.5"
frontend_server_client:
dependency: transitive
description:
@ -781,6 +797,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
iris_method_channel:
dependency: transitive
description:
name: iris_method_channel
sha256: bfb5cfc6c6eae42da8cd1b35977a72d8b8881848a5dfc3d672e4760a907d11a0
url: "https://pub.dev"
source: hosted
version: "2.2.4"
js:
dependency: transitive
description:
@ -1466,6 +1490,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.6"
tobias:
dependency: "direct main"
description:
name: tobias
sha256: "2b5520e622c0d6f04cfb5c9619211f923c97a602e1a3a8954e113e3e0e685c41"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
typed_data:
dependency: transitive
description:

3
pubspec.yaml

@ -62,6 +62,9 @@ dependencies:
video_player: ^2.9.2
chewie: ^1.8.5 # 视频播放器UI
audioplayers: ^6.5.1
fluwx: ^5.7.5
tobias: ^5.3.1
agora_rtc_engine: ^6.5.3
dev_dependencies:
flutter_test:

Loading…
Cancel
Save