From 535bf1ef8eba0e3d0341b3306c15ce7a67e32ba9 Mon Sep 17 00:00:00 2001 From: Jolie <> Date: Sat, 8 Nov 2025 22:39:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=8F=91=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/back_icon.png | Bin 0 -> 3145 bytes assets/images/information_bg.png | Bin 0 -> 12797 bytes assets/images/more_icon.png | Bin 0 -> 3498 bytes assets/images/play_icon.png | Bin 0 -> 1516 bytes assets/images/talk_icon.png | Bin 0 -> 613 bytes assets/images/voice_icon.png | Bin 0 -> 561 bytes lib/controller/home/home_controller.dart | 302 +++++++++ lib/generated/assets.dart | 6 + lib/main.dart | 1 + lib/model/home/marriage_data.dart | 87 +++ lib/network/api_urls.dart | 3 + lib/network/home_api.dart | 23 + lib/network/home_api.g.dart | 89 +++ lib/network/network_service.dart | 6 + lib/network/response_model.dart | 2 +- lib/oss/oss_manager.dart | 2 +- lib/pages/home/content_card.dart | 193 ++++++ lib/pages/home/home_page.dart | 762 ++++++++++++++-------- lib/pages/home/user_information_page.dart | 660 +++++++++++++++++++ lib/pages/main/main_page.dart | 3 +- lib/pages/mine/user_info_page.dart | 1 - pubspec.lock | 24 + pubspec.yaml | 1 + 23 files changed, 1877 insertions(+), 288 deletions(-) create mode 100644 assets/images/back_icon.png create mode 100644 assets/images/information_bg.png create mode 100644 assets/images/more_icon.png create mode 100644 assets/images/play_icon.png create mode 100644 assets/images/talk_icon.png create mode 100644 assets/images/voice_icon.png create mode 100644 lib/controller/home/home_controller.dart create mode 100644 lib/model/home/marriage_data.dart create mode 100644 lib/network/home_api.dart create mode 100644 lib/network/home_api.g.dart create mode 100644 lib/pages/home/content_card.dart create mode 100644 lib/pages/home/user_information_page.dart diff --git a/assets/images/back_icon.png b/assets/images/back_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a4b8749e35418b2788750115798ac26d133d88d GIT binary patch literal 3145 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 literal 0 HcmV?d00001 diff --git a/assets/images/information_bg.png b/assets/images/information_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..f32d540de9aad89b5812b45261d78d42ce6f5aba GIT binary patch literal 12797 zcmeAS@N?(olHy`uVBq!ia0y~yV7*VOnvPzMmWe|rmI zR5U*11G5+p%Ytd9Hfu1=!cz{WIV9eJX#s;fj39>y@y2MgBTNxO14i2!PmIu(W+k?=t+a~DP zEemw)mIFF=D*!Ef6`*bMQ3FN;aWo-8<}pU|!f0L?%?qP>VYF^!P#6t^VG;)3@{&>v z41%*kYXL^HHtmyjdD^+xpr$zk!-H}$Xrp@rtd-u1)+(>i1V;%2BR8~}9swIFKLl;Z zV;BKz=#}?Z0xfu9U|_-si$U%RSYIse6xjU^2XICzx<_Ei^nkY{SSttNbOAF0 z*5Ml}Ned%VM-w;^d1OG67Di4T5LXPf1U?{TCq{WOAg-XG1Rps`3!_3DP2fbdF9sxO zVYIad#1&|bgKcxIH5eJ%8Wm<#t*z%Z+!(-R%=O7`8iPOpY={@uyn{6nM4-)5n1Tc7 zRqg^OjaYxPoSAQyAW)Bxn~*0UOjY@D>g(f5Gj-rC`K( zoCg}?@TN4*2!#bQOo0S6#^Id?^u&WRkPTp68CV+@Bk`cyg;N3fF+Q|Bo&d{tu+9=J zhvE!mn1TUG$>`aN{1_i%9;dwQnlp3f`hIZNuHn9}fCS$QFFEyVAik%opUXO@geCxI C$?tOj literal 0 HcmV?d00001 diff --git a/assets/images/more_icon.png b/assets/images/more_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03f8853a81563df37ad9038699b7a747eab174c2 GIT binary patch literal 3498 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 S+RIh|0000q literal 0 HcmV?d00001 diff --git a/assets/images/talk_icon.png b/assets/images/talk_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8d39ff26ca1211d3797c60397970d242c006166e GIT binary patch literal 613 zcmV-r0-F7aP)Px#1am@3R0s$N2z&@+hyVZr4@pEpRA_9&YGmyXkaQT0zT(w*Edf9e0JTLH zL(<3B77TH+nu=+~_Tu6od*HVa)6zSi7+A||Jcghg|7(I0Heo!jpoC5sk1r@uCXDA1 zlyFlGZy5zis@%FiK}7!YhA#g?a%gK$6&xqa@ZqWG#xsfn0Eoy#OYW)L4z^Sy_m;#4 z2fdd9#?oO6=1P`Da&K93I$zKJ&rlzUJunFrh4dogt6KkoI8Lwa*YR28l@hdrG~@=1 zo^qOAJ5;5r@kqK3&rMm{$!o5!fHo>S%bsc$`VCtH0B|u5Ho&lfl{gqDP)?ZZEfFls z@9=auc8E6di=Mh)Fy?R0&^cf^_gi~vYr+)$ph%TsSiYg1xB!x_MKY@#!wdj)hO|d} z{n~!&F;bu?zLDEalY(fIauVBReUk=BRB$5&{q?%F&hDz53_)oA_amF~c)wS1rFtdBjkWYU+Jh4~`Q8f<|)izHkOJCX#BDs(wQ1U7l~)Eb(E-h7_D zrquiM&c(@Zx(>>&$mMz-YgqB7YWt?&Kg;HwmlaKZd2_-qpGQ*O$Ju`;Wh_c+(R>pB z_l#}StlM|zZEX~Sn3vN~Z@rS)^p4f}W8bd74xV1IZuJ`5@``u?hBKetD_d8bvU|QW z_sk;yYlr_-*nKVhyVdUR$7hvqVmEw4cNZty7Ws8MN;BPl${GLv{IDrnywQ3sq*@;_YAkH)Hsq!T9XjKY4?#jVJ^_[].obs; + // 同城列表数据 + final nearbyFeed = [].obs; + + // 推荐列表的加载状态和分页信息 + final recommendIsLoading = false.obs; + final recommendPage = 1.obs; + final recommendHasMore = true.obs; + + // 同城列表的加载状态和分页信息 + final nearbyIsLoading = false.obs; + final nearbyPage = 1.obs; + final nearbyHasMore = true.obs; + + // 当前标签页索引 + final selectedTabIndex = 0.obs; + + // 分页大小 + final pageSize = 10; + + // 从GetX依赖注入中获取HomeApi实例 + late final HomeApi _homeApi; + + @override + void onInit() { + super.onInit(); + // 从全局依赖中获取HomeApi + _homeApi = Get.find(); + // 初始化时加载数据 + loadInitialData(); + } + + /// 加载初始数据(同时加载两个标签页的数据) + void loadInitialData() async { + // 并行加载两个标签页的数据 + await Future.wait([ + loadRecommendInitialData(), + loadNearbyInitialData(), + ]); + } + + /// 加载推荐列表初始数据 + Future loadRecommendInitialData() async { + if (recommendIsLoading.value) return; + + try { + recommendIsLoading.value = true; + recommendPage.value = 1; + recommendHasMore.value = true; + + // 获取推荐数据 (type=0) + final List items = await _fetchMarriageData( + page: 1, + type: 0, + ); + + // 重置并更新推荐列表 + recommendFeed.clear(); + recommendFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + recommendHasMore.value = items.length >= pageSize; + } catch (e) { + _handleError('获取推荐列表异常', e, '推荐列表加载失败,请稍后重试'); + } finally { + recommendIsLoading.value = false; + } + } + + /// 加载同城列表初始数据 + Future loadNearbyInitialData() async { + if (nearbyIsLoading.value) return; + + try { + nearbyIsLoading.value = true; + nearbyPage.value = 1; + nearbyHasMore.value = true; + + // 获取同城数据 (type=1) + final List items = await _fetchMarriageData( + page: 1, + type: 1, + ); + + // 重置并更新同城列表 + nearbyFeed.clear(); + nearbyFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + nearbyHasMore.value = items.length >= pageSize; + } catch (e) { + _handleError('获取同城列表异常', e, '同城列表加载失败,请稍后重试'); + } finally { + nearbyIsLoading.value = false; + } + } + + /// 加载更多数据 + Future loadMoreData([int? tabIndex]) async { + final targetTab = tabIndex ?? selectedTabIndex.value; + + if (targetTab == 0) { + // 加载推荐列表更多数据 + await loadRecommendMoreData(); + } else { + // 加载同城列表更多数据 + await loadNearbyMoreData(); + } + } + + /// 加载推荐列表更多数据 + Future loadRecommendMoreData() async { + if (recommendIsLoading.value || !recommendHasMore.value) return; + + try { + recommendIsLoading.value = true; + recommendPage.value++; + + // 获取推荐数据 (type=0) + final List items = await _fetchMarriageData( + page: recommendPage.value, + type: 0, + ); + + // 更新推荐列表 + recommendFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + recommendHasMore.value = items.length >= pageSize; + } catch (e) { + recommendPage.value--; // 回退页码 + _handleError('加载推荐更多异常', e, '加载更多失败'); + } finally { + recommendIsLoading.value = false; + } + } + + /// 加载同城列表更多数据 + Future loadNearbyMoreData() async { + if (nearbyIsLoading.value || !nearbyHasMore.value) return; + + try { + nearbyIsLoading.value = true; + nearbyPage.value++; + + // 获取同城数据 (type=1) + final List items = await _fetchMarriageData( + page: nearbyPage.value, + type: 1, + ); + + // 更新同城列表 + nearbyFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + nearbyHasMore.value = items.length >= pageSize; + } catch (e) { + nearbyPage.value--; // 回退页码 + _handleError('加载同城更多异常', e, '加载更多失败'); + } finally { + nearbyIsLoading.value = false; + } + } + + /// 刷新数据 + Future refreshData([int? tabIndex]) async { + final targetTab = tabIndex ?? selectedTabIndex.value; + + if (targetTab == 0) { + // 刷新推荐列表 + await refreshRecommendData(); + } else { + // 刷新同城列表 + await refreshNearbyData(); + } + } + + /// 刷新推荐列表数据 + Future refreshRecommendData() async { + if (recommendIsLoading.value) return; + + try { + recommendIsLoading.value = true; + recommendPage.value = 1; + recommendHasMore.value = true; + + // 获取推荐数据 (type=0) + final List items = await _fetchMarriageData( + page: 1, + type: 0, + ); + + // 更新推荐列表 + recommendFeed.clear(); + recommendFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + recommendHasMore.value = items.length >= pageSize; + } catch (e) { + _handleError('刷新推荐数据异常', e, '刷新失败,请稍后重试'); + } finally { + recommendIsLoading.value = false; + } + } + + /// 刷新同城列表数据 + Future refreshNearbyData() async { + if (nearbyIsLoading.value) return; + + try { + nearbyIsLoading.value = true; + nearbyPage.value = 1; + nearbyHasMore.value = true; + + // 获取同城数据 (type=1) + final List items = await _fetchMarriageData( + page: 1, + type: 1, + ); + + // 更新同城列表 + nearbyFeed.clear(); + nearbyFeed.addAll(items); + + // 根据实际获取的数据量判断是否还有更多数据 + nearbyHasMore.value = items.length >= pageSize; + } catch (e) { + _handleError('刷新同城数据异常', e, '刷新失败,请稍后重试'); + } finally { + nearbyIsLoading.value = false; + } + } + + /// 设置当前标签页 + void setSelectedTabIndex(int index) { + print('Setting selected tab index to: $index'); + selectedTabIndex.value = index; + // 确保UI能够更新 + update(); + } + + /// 获取当前标签页的列表数据 + List getFeedListByTab(int tabIndex) { + return tabIndex == 0 ? List.from(recommendFeed) : List.from(nearbyFeed); + } + + /// 私有方法:获取婚姻数据(统一的数据获取逻辑) + Future> _fetchMarriageData({ + required int page, + required int type, + }) async { + try { + // 调用API获取数据 + var response = await _homeApi.getMarriageList( + page: page, + pageSize: pageSize, + type: type, + ); + + if (response.data.isSuccess && response.data.data != null) { + // 根据API返回结构解析数据 + final data = response.data.data; + + // 检查data是否包含列表数据 + if (data is List) { + // 如果data直接是列表,直接映射为MarriageData + return data.map((item) => MarriageData.fromJson(item as Map)).toList(); + } else if (data is Map) { + // 如果data是对象,检查是否有list或records字段 + final listData = data['list'] ?? data['records']; + if (listData is List) { + return listData.map((item) => MarriageData.fromJson(item as Map)).toList(); + } + } + + // 如果无法解析为有效列表,返回空列表 + return []; + } else { + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch (e) { + // 向上抛出异常,让调用方处理 + rethrow; + } + } + + /// 私有方法:统一的错误处理 + void _handleError(String logMessage, dynamic error, String toastMessage) { + // 打印错误日志 + print('$logMessage: $error'); + // 显示错误提示 + SmartDialog.showToast(toastMessage); + } +} \ No newline at end of file diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 3307706..27a7b80 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -27,5 +27,11 @@ class Assets { static const String imagesVerifiedIcon = 'assets/images/verified_icon.png'; static const String imagesArrowForwardRight = 'assets/images/arrow_forward_right.png'; static const String imagesExampleContent = 'assets/images/example_content.png'; + static const String imagesBackIcon = 'assets/images/back_icon.png'; + static const String imagesInformationBg = 'assets/images/information_bg.png'; + static const String imagesMoreIcon = 'assets/images/more_icon.png'; + static const String imagesPlayIcon = 'assets/images/play_icon.png'; + static const String imagesTalkIcon = 'assets/images/talk_icon.png'; + static const String imagesVoiceIcon = 'assets/images/voice_icon.png'; } diff --git a/lib/main.dart b/lib/main.dart index 36f4a85..81fcfc5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ void main() async { final networkService = NetworkService(); Get.put(networkService); Get.put(networkService.userApi); + Get.put(networkService.homeApi); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( diff --git a/lib/model/home/marriage_data.dart b/lib/model/home/marriage_data.dart new file mode 100644 index 0000000..1405b89 --- /dev/null +++ b/lib/model/home/marriage_data.dart @@ -0,0 +1,87 @@ +// 数据模型类 - 根据真实API返回格式调整 +class MarriageData { + final String miId; + final String userId; + final String profilePhoto; + final String nickName; + final bool isRealNameCertified; + final String birthYear; + final String birthDate; + final int age; + final int provinceCode; + final String provinceName; + final int cityCode; + final String cityName; + final int districtCode; + final String districtName; + final String describeInfo; + final String createTime; + final List photoList; + + // 为了兼容UI展示,添加一些计算属性 + String get name => nickName; + String get avatar => profilePhoto.trim().replaceAll('`', ''); // 移除照片URL中的反引号 + String get city => cityName; + String get description => describeInfo; + List get images => photoList.map((photo) => photo.photoUrl.trim().replaceAll('`', '')).toList(); + + MarriageData({ + required this.miId, + required this.userId, + required this.profilePhoto, + required this.nickName, + required this.isRealNameCertified, + required this.birthYear, + required this.birthDate, + required this.age, + required this.provinceCode, + required this.provinceName, + required this.cityCode, + required this.cityName, + required this.districtCode, + required this.districtName, + required this.describeInfo, + required this.createTime, + required this.photoList, + }); + + factory MarriageData.fromJson(Map json) { + return MarriageData( + miId: json['miId'] ?? '', + userId: json['userId'] ?? '', + profilePhoto: json['profilePhoto'] ?? '', + nickName: json['nickName'] ?? '', + isRealNameCertified: json['isRealNameCertified'] ?? false, + birthYear: json['birthYear'] ?? '', + birthDate: json['birthDate'] ?? '', + age: json['age'] ?? 0, + provinceCode: json['provinceCode'] ?? 0, + provinceName: json['provinceName'] ?? '', + cityCode: json['cityCode'] ?? 0, + cityName: json['cityName'] ?? '', + districtCode: json['districtCode'] ?? 0, + districtName: json['districtName'] ?? '', + describeInfo: json['describeInfo'] ?? '', + createTime: json['createTime'] ?? '', + photoList: (json['photoList'] as List?)?.map((e) => PhotoItem.fromJson(e as Map)).toList() ?? [], + ); + } +} + +// 照片项数据模型 +class PhotoItem { + final String photoUrl; + final dynamic auditStatus; + + PhotoItem({ + required this.photoUrl, + this.auditStatus, + }); + + factory PhotoItem.fromJson(Map json) { + return PhotoItem( + photoUrl: json['photoUrl'] ?? '', + auditStatus: json['auditStatus'], + ); + } +} \ No newline at end of file diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index cffc6de..c939f15 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -12,6 +12,9 @@ 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 getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information'; // 后续可以在此添加更多API端点 } \ No newline at end of file diff --git a/lib/network/home_api.dart b/lib/network/home_api.dart new file mode 100644 index 0000000..a36fb56 --- /dev/null +++ b/lib/network/home_api.dart @@ -0,0 +1,23 @@ +import 'package:dating_touchme_app/network/api_urls.dart'; +import 'package:dating_touchme_app/network/response_model.dart'; +import 'package:retrofit/retrofit.dart'; +import 'package:dio/dio.dart'; + +part 'home_api.g.dart'; + +/// 首页相关API接口定义 +@RestApi(baseUrl: '') +abstract class HomeApi { + factory HomeApi(Dio dio) = _HomeApi; + + /// 获取用户列表 + /// [page] - 页码 + /// [pageSize] - 每页数量 + /// [type] - 类型:0-推荐,1-同城 + @GET(ApiUrls.getMarriageList) + Future>> getMarriageList({ + @Query('page') required int page, + @Query('pageSize') required int pageSize, + @Query('type') required int type, + }); +} \ No newline at end of file diff --git a/lib/network/home_api.g.dart b/lib/network/home_api.g.dart new file mode 100644 index 0000000..8934e10 --- /dev/null +++ b/lib/network/home_api.g.dart @@ -0,0 +1,89 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_api.dart'; + +// dart format off + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations,unused_element_parameter,avoid_unused_constructor_parameters,unreachable_from_main + +class _HomeApi implements HomeApi { + _HomeApi(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future>> getMarriageList({ + required int page, + required int pageSize, + required int type, + }) async { + final _extra = {}; + final queryParameters = { + r'page': page, + r'pageSize': pageSize, + r'type': type, + }; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/page/dongwo/marriage-information', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} + +// dart format on diff --git a/lib/network/network_service.dart b/lib/network/network_service.dart index b1dfb19..5ef54a2 100644 --- a/lib/network/network_service.dart +++ b/lib/network/network_service.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'api_service.dart'; +import 'home_api.dart'; import 'network_config.dart'; import 'user_api.dart'; @@ -12,6 +13,7 @@ class NetworkService { // API服务实例 late final ApiService _apiService; // 主API服务 late final UserApi _userApi; + late final HomeApi _homeApi; /// 获取单例实例 factory NetworkService() { @@ -23,6 +25,7 @@ class NetworkService { final dio = NetworkConfig.createDio(); _apiService = ApiService(dio); _userApi = UserApi(dio); + _homeApi = HomeApi(dio); } /// 通用GET请求 @@ -158,6 +161,9 @@ class NetworkService { /// 用户相关API UserApi get userApi => _userApi; + + /// 首页相关API + HomeApi get homeApi => _homeApi; void _showLoading() { // 使用FlutterSmartDialog显示加载指示器 diff --git a/lib/network/response_model.dart b/lib/network/response_model.dart index 43cfe1f..31150da 100644 --- a/lib/network/response_model.dart +++ b/lib/network/response_model.dart @@ -77,7 +77,7 @@ class PaginatedResponse { Map toJson(Object Function(T) toJsonT) { return { - 'list': list.map((e) => toJsonT(e)).toList(), + 'records': list.map((e) => toJsonT(e)).toList(), 'total': total, 'page': page, 'pageSize': pageSize, diff --git a/lib/oss/oss_manager.dart b/lib/oss/oss_manager.dart index d13109c..2301db3 100644 --- a/lib/oss/oss_manager.dart +++ b/lib/oss/oss_manager.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:flustars/flustars.dart'; import 'package:flutter_oss_aliyun/flutter_oss_aliyun.dart'; import 'package:dating_touchme_app/network/network_service.dart'; -import 'package:dating_touchme_app/model/common/oss_data.dart'; + import 'package:get/get.dart'; import '../network/user_api.dart'; diff --git a/lib/pages/home/content_card.dart b/lib/pages/home/content_card.dart new file mode 100644 index 0000000..e4fe7de --- /dev/null +++ b/lib/pages/home/content_card.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/model/home/marriage_data.dart'; + +class ContentCard extends StatelessWidget { + final MarriageData item; + + const ContentCard({ + Key? key, + required this.item, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + margin: EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 用户信息头部 + _buildUserHeader(), + + // 个人描述 + if (item.description.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text( + item.description, + style: TextStyle( + fontSize: 14, + color: Colors.black87 + ), + strutStyle: const StrutStyle(height: 1.5), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ) + ), + + // 照片列表 + if (item.photoList.isNotEmpty) + _buildImageGrid(), + + ], + ), + ); + } + + Widget _buildUserHeader() { + return Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 用户头像 + CircleAvatar( + radius: 30, + backgroundImage: NetworkImage(_cleanImageUrl(item.avatar)), + backgroundColor: Colors.grey[200], + ), + + // 用户信息 + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + item.nickName, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black + ), + ), + SizedBox(width: 6), + Text( + '${item.age}岁', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600] + ), + ), + ], + ), + Text( + item.city, + style: TextStyle( + fontSize: 12, + color: Colors.grey[500] + ), + strutStyle: const StrutStyle(height: 1.2), + ) + ], + ), + ), + ), + + // 打招呼按钮 + GestureDetector( + onTap: () { + // 打招呼功能 + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: Color(0xFFFF6B6B), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Image.asset( + Assets.imagesHiIcon, + width: 16, + height: 16, + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildImageGrid() { + int imageCount = item.photoList.length; + + if (imageCount == 0) return SizedBox(); + + // 单张图片 + if (imageCount == 1) { + return Container( + width: double.infinity, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Image.network( + _cleanImageUrl(item.photoList[0].photoUrl), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + Assets.imagesExampleContent, + fit: BoxFit.cover, + ); + }, + ), + ); + } + + // 多张图片网格布局 + return GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 2, + crossAxisSpacing: 2, + ), + itemCount: imageCount, + itemBuilder: (context, index) { + return Container( + height: 100, + child: Image.network( + _cleanImageUrl(item.photoList[index].photoUrl), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + Assets.imagesExampleContent, + fit: BoxFit.cover, + ); + }, + ), + ); + }, + ); + } + + // 清理图片URL中的空格和多余字符 + String _cleanImageUrl(String url) { + return url.trim(); + } +} \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index a786c09..a5139d7 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,80 +1,91 @@ import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:dating_touchme_app/controller/home/home_controller.dart'; +import 'package:dating_touchme_app/model/home/marriage_data.dart'; +import 'package:dating_touchme_app/pages/home/user_information_page.dart'; class HomePage extends StatefulWidget { - const HomePage({super.key}); + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); + } - @override - State createState() => _HomePageState(); -} class _HomePageState extends State with AutomaticKeepAliveClientMixin { - int selectedTabIndex = 0; // 0: 推荐 1: 同城 - - // 简单的示例数据结构,使用更安全的图片数据 - final List<_FeedItem> recommendFeed = [ - _FeedItem(type: _FeedType.offline, content: '我想找一个有缘的异性,快来联系我吧快来……'), // 不设置图片 - _FeedItem(type: _FeedType.online, content: '我想找一个有缘的异性,快来联系我吧快来……'), - _FeedItem(type: _FeedType.live, content: '正在直播,快来互动~'), - _FeedItem(type: _FeedType.offline, content: '大家好,很高兴认识新朋友!'), // 不设置图片 - ]; + // 使用GetX注入HomeController + final HomeController ctrl = Get.put(HomeController()); + // 推荐列表的刷新控制器 + late final EasyRefreshController _recommendRefreshController; + // 同城列表的刷新控制器 + late final EasyRefreshController _nearbyRefreshController; + + @override + void initState() { + super.initState(); + // 初始化推荐列表刷新控制器 + _recommendRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + // 初始化同城列表刷新控制器 + _nearbyRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + } - final List<_FeedItem> nearbyFeed = [ - _FeedItem(type: _FeedType.online, content: '同城的朋友,有空一起出来玩呀~'), - _FeedItem(type: _FeedType.offline, content: '周末有什么好去处推荐吗?'), // 不设置图片 - _FeedItem(type: _FeedType.live, content: '正在直播,快来互动~'), - ]; + @override + void dispose() { + _recommendRefreshController.dispose(); + _nearbyRefreshController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { super.build(context); - final List<_FeedItem> dataSource = - selectedTabIndex == 0 ? recommendFeed : nearbyFeed; - - // 获取 AppBar 高度(状态栏 + toolbar + bottom) - final statusBarHeight = MediaQuery.of(context).padding.top; - final appBarHeight = statusBarHeight + 56 + 4; // toolbarHeight 56 + bottom 4 - - // 获取底部安全区域高度和 tabbar 高度(约64) - final bottomPadding = MediaQuery.of(context).padding.bottom; - final tabBarHeight = 64.0; - final totalBottomPadding = bottomPadding + tabBarHeight; + return GetBuilder( + builder: (controller) { + // 获取底部安全区域高度和 tabbar 高度(约64) + final bottomPadding = MediaQuery.of(context).padding.bottom; + final tabBarHeight = 64.0; + final totalBottomPadding = bottomPadding + tabBarHeight; - return Stack( - children: [ - // 背景图 - 覆盖整个屏幕包括状态栏和导航栏 - Image.asset( - Assets.imagesBgInformation, - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - ), - Scaffold( - backgroundColor: Colors.transparent, - appBar: _buildAppBar(), - body: SafeArea( - child: ListView.separated( - physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - itemBuilder: (context, index) { - final item = dataSource[index]; - return ContentCard(item: item); - }, - separatorBuilder: (context, index) => const SizedBox(height: 12), - itemCount: dataSource.length, - padding: EdgeInsets.only( - left: 12, - right: 12, - top: 12, // 从 AppBar 下方开始 - bottom: totalBottomPadding + 12, // 避免被 tabbar 遮挡 - ), + return Stack( + children: [ + // 背景图 - 覆盖整个屏幕包括状态栏和导航栏 + Image.asset( + Assets.imagesBgInformation, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, ), - ), - ), - ], - ); + Scaffold( + backgroundColor: Colors.transparent, + appBar: _buildAppBar(controller), + body: Obx(() { + // 使用 IndexedStack 保持两个列表的状态,根据当前选中的标签显示对应的列表 + return IndexedStack( + index: controller.selectedTabIndex.value, + children: [ + // 推荐列表 + _buildRecommendList(controller, totalBottomPadding), + // 同城列表 + _buildNearbyList(controller, totalBottomPadding), + ], + ); + }), + ), + ], + ); + } + ); } - PreferredSizeWidget _buildAppBar() { + PreferredSizeWidget _buildAppBar(HomeController controller) { return AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -85,9 +96,9 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - _buildTabButton(title: '推荐', index: 0), + _buildTabButton(title: '推荐', index: 0, controller: controller), const SizedBox(width: 28), - _buildTabButton(title: '同城', index: 1), + _buildTabButton(title: '同城', index: 1, controller: controller), ], ), // actions: [ @@ -107,14 +118,15 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin ); } - Widget _buildTabButton({required String title, required int index}) { - final bool selected = selectedTabIndex == index; + Widget _buildTabButton({required String title, required int index, required HomeController controller}) { + final bool selected = controller.selectedTabIndex.value == index; return GestureDetector( onTap: () { - if (selectedTabIndex != index) { - setState(() { - selectedTabIndex = index; - }); + print('Tab $index clicked'); + if (controller.selectedTabIndex.value != index) { + controller.setSelectedTabIndex(index); + // 确保状态更新后刷新UI + controller.update(); } }, child: Column( @@ -142,80 +154,229 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin ); } - // 下面将卡片组件拆分为独立Widget,保持HomePage更简洁 + // 构建推荐列表 + Widget _buildRecommendList(HomeController controller, double totalBottomPadding) { + return Obx(() { + final List dataSource = controller.recommendFeed; + final bool isLoading = controller.recommendIsLoading.value; + final bool hasMore = controller.recommendHasMore.value; + + return EasyRefresh( + controller: _recommendRefreshController, + header: MaterialHeader( + backgroundColor: Colors.red.withOpacity(0.9), + ), + footer: MaterialFooter( + backgroundColor: Colors.red.withOpacity(0.9), + ), + // 下拉刷新 + onRefresh: () async { + print('推荐列表下拉刷新被触发'); + try { + await controller.refreshRecommendData(); + _recommendRefreshController.finishRefresh(IndicatorResult.success); + print('推荐列表刷新完成'); + } catch (e) { + print('推荐列表刷新失败: $e'); + _recommendRefreshController.finishRefresh(IndicatorResult.fail); + } + }, + // 上拉加载更多 + onLoad: () async { + print('推荐列表上拉加载被触发, hasMore: $hasMore'); + if (hasMore && controller.recommendHasMore.value) { + try { + await controller.loadRecommendMoreData(); + // 完成加载,根据是否有更多数据决定 + if (controller.recommendHasMore.value) { + _recommendRefreshController.finishLoad(IndicatorResult.success); + print('推荐列表加载更多成功'); + } else { + _recommendRefreshController.finishLoad(IndicatorResult.noMore); + print('推荐列表没有更多数据了'); + } + } catch (e) { + print('推荐列表加载更多失败: $e'); + _recommendRefreshController.finishLoad(IndicatorResult.fail); + } + } else { + _recommendRefreshController.finishLoad(IndicatorResult.noMore); + print('推荐列表没有更多数据'); + } + }, + // EasyRefresh 的 child 必须始终是可滚动的 Widget + child: ListView.separated( + // 关键:始终允许滚动,即使内容不足 + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + // 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 + padding: EdgeInsets.only( + left: 12, + right: 12, + bottom: totalBottomPadding + 12, + ), + itemBuilder: (context, index) { + // 加载状态 + if (isLoading && dataSource.isEmpty && index == 0) { + // 使用足够的高度确保可以滚动 + return SizedBox( + height: MediaQuery.of(context).size.height * 1.2, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + } + // 空数据状态 + if (!isLoading && dataSource.isEmpty && index == 0) { + // 使用足够的高度确保可以滚动 + return SizedBox( + height: MediaQuery.of(context).size.height * 1.2, + child: const Center( + child: Text( + "暂无数据", + style: TextStyle( + fontSize: 14, + color: Color(0xFF999999), + ), + ), + ), + ); + } + // 数据项 + final item = dataSource[index]; + return ContentCard(item: item); + }, + separatorBuilder: (context, index) { + // 空状态或加载状态时不显示分隔符 + if (dataSource.isEmpty) return const SizedBox.shrink(); + return const SizedBox(height: 12); + }, + // 至少显示一个 item(用于显示加载或空状态) + itemCount: dataSource.isEmpty ? 1 : dataSource.length, + ), + ); + }); + } + + // 构建同城列表 + Widget _buildNearbyList(HomeController controller, double totalBottomPadding) { + return Obx(() { + final List dataSource = controller.nearbyFeed; + final bool isLoading = controller.nearbyIsLoading.value; + final bool hasMore = controller.nearbyHasMore.value; + + return EasyRefresh( + controller: _nearbyRefreshController, + header: MaterialHeader( + backgroundColor: Colors.red.withOpacity(0.9), + ), + footer: MaterialFooter( + backgroundColor: Colors.red.withOpacity(0.9), + ), + // 下拉刷新 + onRefresh: () async { + print('同城列表下拉刷新被触发'); + try { + await controller.refreshNearbyData(); + _nearbyRefreshController.finishRefresh(IndicatorResult.success); + print('同城列表刷新完成'); + } catch (e) { + print('同城列表刷新失败: $e'); + _nearbyRefreshController.finishRefresh(IndicatorResult.fail); + } + }, + // 上拉加载更多 + onLoad: () async { + print('同城列表上拉加载被触发, hasMore: $hasMore'); + if (hasMore && controller.nearbyHasMore.value) { + try { + await controller.loadNearbyMoreData(); + // 完成加载,根据是否有更多数据决定 + if (controller.nearbyHasMore.value) { + _nearbyRefreshController.finishLoad(IndicatorResult.success); + print('同城列表加载更多成功'); + } else { + _nearbyRefreshController.finishLoad(IndicatorResult.noMore); + print('同城列表没有更多数据了'); + } + } catch (e) { + print('同城列表加载更多失败: $e'); + _nearbyRefreshController.finishLoad(IndicatorResult.fail); + } + } else { + _nearbyRefreshController.finishLoad(IndicatorResult.noMore); + print('同城列表没有更多数据'); + } + }, + // EasyRefresh 的 child 必须始终是可滚动的 Widget + child: ListView.separated( + // 关键:始终允许滚动,即使内容不足 + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + // 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 + padding: EdgeInsets.only( + left: 12, + right: 12, + bottom: totalBottomPadding + 12, + ), + itemBuilder: (context, index) { + // 加载状态 + if (isLoading && dataSource.isEmpty && index == 0) { + // 使用足够的高度确保可以滚动 + return SizedBox( + height: MediaQuery.of(context).size.height * 1.2, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + } + // 空数据状态 + if (!isLoading && dataSource.isEmpty && index == 0) { + // 使用足够的高度确保可以滚动 + return SizedBox( + height: MediaQuery.of(context).size.height * 1.2, + child: const Center( + child: Text( + "暂无数据", + style: TextStyle( + fontSize: 14, + color: Color(0xFF999999), + ), + ), + ), + ); + } + // 数据项 + final item = dataSource[index]; + return ContentCard(item: item); + }, + separatorBuilder: (context, index) { + // 空状态或加载状态时不显示分隔符 + if (dataSource.isEmpty) return const SizedBox.shrink(); + return const SizedBox(height: 12); + }, + // 至少显示一个 item(用于显示加载或空状态) + itemCount: dataSource.isEmpty ? 1 : dataSource.length, + ), + ); + }); + } @override bool get wantKeepAlive => true; } -// 卡片类型枚举 -enum _FeedType { - online, // 在线 + HI按钮 - offline, // 下线 + 发消息按钮 - live // 直播 + 直播间按钮(放在HI按钮位置) -} - -// 接口数据模型 -class _FeedItem { - final _FeedType type; - final String? nickname; // 昵称 - final String? avatar; // 头像URL - final int? age; // 年龄 - final String? location; // 地区 - final bool? isVerified; // 是否实名认证 - final String? content; // 内容文本 - final List? images; // 图片列表 - // 从接口数据构造 - const _FeedItem({ - required this.type, - this.nickname = '林园园', - this.avatar, - this.age = 23, - this.location = '白云区', - this.isVerified = true, - this.content = '我想找一个有缘的异性,快来联系我吧快来……', - this.images, - }); - - // 模拟从API响应创建实例的工厂方法 - factory _FeedItem.fromApi(Map apiData) { - // 根据接口参数判断类型 - final bool isLive = apiData['isLive'] ?? false; - final bool isOnline = apiData['isOnline'] ?? false; - - _FeedType type; - if (isLive) { - type = _FeedType.live; - } else if (isOnline) { - type = _FeedType.online; - } else { - type = _FeedType.offline; - } - - return _FeedItem( - type: type, - nickname: apiData['nickname'], - avatar: apiData['avatar'], - age: apiData['age'], - location: apiData['location'], - isVerified: apiData['isVerified'], - content: apiData['content'], - images: List.from(apiData['images'] ?? []), - // viewerCount已移除 - ); - } -} - // 通用头部组件:头像/昵称/在线/认证/Hi/直播中徽标 - class _CardHeader extends StatelessWidget { - final _FeedItem item; + final MarriageData item; const _CardHeader({required this.item}); @override Widget build(BuildContext context) { - final bool isLive = item.type == _FeedType.live; - final bool isOnline = item.type == _FeedType.online; + final bool isLive = true;//item.isLive ?? false; + final bool isOnline = true;//item.isOnline ?? false; return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -298,71 +459,83 @@ class _CardHeader extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - item.nickname ?? '用户', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - color: Color.fromRGBO(51, 51, 51, 1), - ), - ), - const SizedBox(width: 6), - // 在线/离线徽标 - if (isOnline == true) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Color.fromRGBO(234, 255, 219, 1) , - borderRadius: BorderRadius.circular(12), - ), - child: Text( - isOnline ? '在线' : '', - style: TextStyle(fontSize: 9, color: Color.fromRGBO(38, 199, 124, 1) ), - ), - ), - const SizedBox(width: 6), - // 实名徽标 - if (item.isVerified == true) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFFF3E9FF), - borderRadius: BorderRadius.circular(12), + // 使用Wrap组件让所有标签在空间允许时显示在一行,空间不足时自动换行 + Wrap( + spacing: 6, + runSpacing: 2, + children: [ + // 用户名 + Text( + item.name ?? '用户', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: Color.fromRGBO(51, 51, 51, 1)), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - child: Row( - children: [ - Image.asset(Assets.imagesVerifiedIcon, width: 14, height: 12), - const SizedBox(width: 4), - const Text( - '实名', - style: TextStyle(fontSize: 9, color: Color.fromRGBO(160, 92, 255, 1)), + // 在线徽标 + if (isOnline == true) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Color.fromRGBO(234, 255, 219, 1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + isOnline ? '在线' : '', + style: TextStyle(fontSize: 9, color: Color.fromRGBO(38, 199, 124, 1) ), + ), + ), + // 实名徽标 + if (item.isRealNameCertified == true) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF3E9FF), + borderRadius: BorderRadius.circular(12), + ), + constraints: BoxConstraints( + minWidth: 40, // 设置最小宽度以保证视觉效果 + ), + child: Row( + mainAxisSize: MainAxisSize.min, // 确保Row只占用必要的宽度 + children: [ + Image.asset(Assets.imagesVerifiedIcon, width: 14, height: 12), + const SizedBox(width: 4), + const Text( + '实名', + style: TextStyle(fontSize: 9, color: Color.fromRGBO(160, 92, 255, 1)), + ), + ], ), - ], ), + // 直播状态下显示视频相亲中标签 + if (isLive) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Color.fromRGBO(234, 255, 219, 1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '视频相亲中', + style: TextStyle(fontSize: 9, color: Color.fromRGBO(38, 199, 124, 1)), + ), + ), + ], ), - const SizedBox(width: 6), - // 直播状态下显示视频相亲中标签 - if (isLive) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Color.fromRGBO(234, 255, 219, 1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '视频相亲中', - style: TextStyle(fontSize: 9, color: Color.fromRGBO(38, 199, 124, 1)), - ), - ), ], ), - const SizedBox(height: 4), - Text( - '${item.age}岁 · ${item.location}', - style: TextStyle(fontSize: 12, color: Color.fromRGBO(51, 51, 51, 1)), - ), + const SizedBox(height: 6), + SizedBox( + height: 16, + child: Text( + '${item.age ?? 0}岁 · ${item.cityName ?? ''}${item.districtName != null ? item.districtName : ''}', + style: TextStyle(fontSize: 12, color: Color.fromRGBO(51, 51, 51, 1)), + overflow: TextOverflow.ellipsis, + ), + ) ], ), ), @@ -382,110 +555,134 @@ class _CardHeader extends StatelessWidget { } }, child: Image.asset( - _getButtonImage(), - width: item.type == _FeedType.live ? 60 : 40, - height: 20, - ), + _getButtonImage(), + // width: (item.isLive ?? false) ? 60 : 40, + width: (true) ? 60 : 40, + height: 20, + ), ), ], ); } String _getButtonImage() { - switch (item.type) { - case _FeedType.offline: - return Assets.imagesSendMessageIcon; // 下线显示发消息按钮(放在HI位置) - case _FeedType.live: - return Assets.imagesLiveIcon; // 直播显示直播间按钮(放在HI位置) - case _FeedType.online: - default: - return Assets.imagesHiIcon; // 在线显示HI按钮 - } + // if (item.isLive ?? false) { + // return Assets.imagesLiveIcon; // 直播显示直播间按钮(放在HI位置) + // } else if (item.isOnline ?? false) { + // return Assets.imagesHiIcon; // 在线显示HI按钮 + // } else { + // return Assets.imagesSendMessageIcon; // 下线显示发消息按钮(放在HI位置) + // } + // } + + return Assets.imagesLiveIcon; // 直播显示直播间按钮(放在HI位置) } + } // 统一的内容卡片组件,根据接口数据显示不同状态 class ContentCard extends StatelessWidget { - final _FeedItem item; + final MarriageData item; const ContentCard({required this.item}); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 8, - offset: Offset(0, 4), - ), - ], - ), - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 头部:头像、昵称、在线状态、按钮等 - 所有按钮都放在HI位置 - _CardHeader(item: item), - const SizedBox(height: 8), - - // 内容区域 - 根据类型显示不同内容 - _buildContent(), - ], + return GestureDetector( + onTap: () { + // 点击卡片跳转到用户信息页面,传递用户数据 + Get.to(() => UserInformationPage(userData: item)); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x14000000), + blurRadius: 8, + offset: Offset(0, 4), + ), + ], + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 头部:头像、昵称、在线状态、按钮等 - 所有按钮都放在HI位置 + _CardHeader(item: item), + const SizedBox(height: 8), + + // 内容区域 - 根据类型显示不同内容 + _buildContent(), + ], + ), ), ); } Widget _buildContent() { - final List contentWidgets = []; - - // 内容文本 - if (item.content != null && item.content!.isNotEmpty) { - contentWidgets.add( - Text( - item.content!, - style: TextStyle( - fontSize: 13, - color: Color.fromRGBO(51, 51, 51, 0.6), - ), - ), - ); - } - - // 根据接口返回的图片列表动态显示图片 - if (item.images != null && item.images!.isNotEmpty) { - contentWidgets.add(const SizedBox(height: 16)); - - // 根据图片数量决定显示方式 - if (item.images!.length == 1) { - // 单张图片:显示大图 - contentWidgets.add( - _NetworkImageWidget(imageUrl: item.images![0], aspectRatio: 2.5), - ); - } else { - // 多张图片:最多显示3张 - contentWidgets.add( - Row( - children: item.images!.take(3).toList().asMap().entries.map((entry) { - int index = entry.key; - String imageUrl = entry.value; - return Expanded( - child: Padding( - padding: EdgeInsets.only(left: index > 0 ? 12 : 0), - child: _NetworkImageWidget(imageUrl: imageUrl, aspectRatio: 1.05), + return Builder( + builder: (context) { + final List contentWidgets = []; + // 内容文本 + if (item.describeInfo != null && item.describeInfo!.isNotEmpty) { + contentWidgets.add( + SizedBox( + // height: 20, // 固定高度20 + child: Text( + item.describeInfo!, + style: TextStyle( + fontSize: 13, + color: Color.fromRGBO(51, 51, 51, 0.6), ), - ); - }).toList(), - ), + overflow: TextOverflow.ellipsis, // 文本超出显示... + maxLines: 1, // 限制为单行 + ), + ), + ); + } + + // 根据接口返回的图片列表动态显示图片 + if (item.photoList != null && item.photoList!.isNotEmpty) { + contentWidgets.add(const SizedBox(height: 16)); + + // 计算固定宽度:每张图片的宽度 = (屏幕宽度 - 卡片左右padding - ListView左右padding - 图片间距) / 3 + // 卡片左右padding: 12 * 2 = 24 + // ListView左右padding: 12 * 2 = 24 + // 3张图片时,间距: 12 * 2 = 24 (第2张和第3张左边各12) + final screenWidth = MediaQuery.of(context).size.width; + final cardPadding = 12.0 * 2; // 卡片左右padding + final listPadding = 12.0 * 2; // ListView左右padding + final imageSpacing = 12.0 * 2; // 3张图片时的总间距 + final imageWidth = (screenWidth - cardPadding - listPadding - imageSpacing) / 3; + final imageHeight = imageWidth * 1.05; // aspectRatio 1.05 + + // 统一使用相同的布局:无论1张、2张还是3张,都使用相同的显示方式和大小 + // 最多显示3张,按顺序排列 + final displayPhotos = item.photoList!.take(3).toList(); + contentWidgets.add( + Row( + children: displayPhotos.asMap().entries.map((entry) { + int index = entry.key; + String imageUrl = entry.value.photoUrl ?? ''; + return Padding( + padding: EdgeInsets.only(left: index > 0 ? 12 : 0), + child: SizedBox( + width: imageWidth, + height: imageHeight, + child: _NetworkImageWidget(imageUrl: imageUrl, aspectRatio: 1.05), + ), + ); + }).toList(), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: contentWidgets, ); - } - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: contentWidgets, + }, ); } } @@ -497,22 +694,21 @@ class _NetworkImageWidget extends StatelessWidget { final String imageUrl; final double aspectRatio; const _NetworkImageWidget({required this.imageUrl, required this.aspectRatio}); - + @override Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: aspectRatio, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - ), - clipBehavior: Clip.antiAlias, - child: Image.asset( - // 暂时使用本地图片替代网络图片,避免网络加载失败导致闪退 - Assets.imagesExampleContent, + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageUrl.isNotEmpty + ? NetworkImage(imageUrl) + : AssetImage(Assets.imagesAvatarsExample) as ImageProvider, fit: BoxFit.cover, ), ), + width: double.infinity, + height: double.infinity, ); } } diff --git a/lib/pages/home/user_information_page.dart b/lib/pages/home/user_information_page.dart new file mode 100644 index 0000000..8bc4bc8 --- /dev/null +++ b/lib/pages/home/user_information_page.dart @@ -0,0 +1,660 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/model/home/marriage_data.dart'; + +class UserInformationPage extends StatefulWidget { + final MarriageData userData; + + const UserInformationPage({super.key, required this.userData}); + + @override + State createState() => _UserInformationPageState(); +} + +class _UserInformationPageState extends State { + bool _showMoreMenu = false; + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + // 按照 750:769 的比例计算高度 + final topSectionHeight = screenWidth * 769 / 750; + + return Scaffold( + backgroundColor: Colors.white, + body: Stack( + children: [ + // Top section with profile image + _buildTopSection(topSectionHeight, screenWidth), + + // Scrollable content section + _buildScrollableContent(topSectionHeight, screenHeight), + + // Bottom action bar + _buildBottomActionBar(), + ], + ), + ); + } + + Widget _buildTopSection(double height, double width) { + return GestureDetector( + onTap: () { + // Close dropdown menu when tapping on background + if (_showMoreMenu) { + setState(() { + _showMoreMenu = false; + }); + } + }, + child: Stack( + children: [ + // Main profile image + widget.userData.avatar.isNotEmpty + ? Image.network( + widget.userData.avatar, + width: width, + height: height, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + Assets.imagesAvatarsExample, + width: width, + height: height, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + Assets.imagesAvatarsExample, + width: width, + height: height, + fit: BoxFit.cover, + ), + // Add imagesInformationBg overlay with same size + Image.asset( + Assets.imagesInformationBg, + width: width, + height: height, + fit: BoxFit.cover, + ), + + // Back button + Positioned( + top: MediaQuery.of(context).padding.top + 8, + left: 16, + child: GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + Assets.imagesBackIcon, + width: 24, + height: 24, + color: Colors.white, + ), + ), + ), + ), + ), + + // More button with dropdown menu + Positioned( + top: MediaQuery.of(context).padding.top + 8, + right: 16, + child: GestureDetector( + onTap: () { + setState(() { + _showMoreMenu = !_showMoreMenu; + }); + }, + behavior: HitTestBehavior.opaque, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + shape: BoxShape.circle, + ), + child: Center( + child: Image.asset( + Assets.imagesMoreIcon, + width: 24, + height: 24, + color: Colors.white, + ), + ), + ), + + // Dropdown menu + if (_showMoreMenu) + Positioned( + top: 50, + right: 0, + child: GestureDetector( + onTap: () {}, // Prevent event bubbling + behavior: HitTestBehavior.opaque, + child: Material( + color: Colors.transparent, + child: Container( + width: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildMenuItem('举报', () { + setState(() => _showMoreMenu = false); + // Handle report action + }), + Divider(height: 1, color: Colors.grey[200]), + _buildMenuItem('拉黑', () { + setState(() => _showMoreMenu = false); + // Handle blacklist action + }), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + + // Three small profile pictures at bottom-left + Positioned( + bottom: -30, + left: 16, + child: Row( + children: widget.userData.photoList.isNotEmpty + ? widget.userData.photoList.take(3).map((photo) { + return Container( + margin: const EdgeInsets.only(right: 8), + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: ClipOval( + child: photo.photoUrl.isNotEmpty + ? Image.network( + photo.photoUrl, + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + Assets.imagesAvatarsExample, + width: 60, + height: 60, + fit: BoxFit.cover, + ); + }, + ) + : Image.asset( + Assets.imagesAvatarsExample, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + ); + }).toList() + : List.generate( + 3, + (index) => Container( + margin: const EdgeInsets.only(right: 8), + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + child: ClipOval( + child: Image.asset( + Assets.imagesAvatarsExample, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildMenuItem(String text, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text( + text, + style: const TextStyle( + fontSize: 14, + color: Colors.black, + ), + ), + ), + ); + } + + Widget _buildScrollableContent(double topSectionHeight, double screenHeight) { + // 计算底部操作栏的高度(包括安全区域) + final bottomBarHeight = 56 + MediaQuery.of(context).padding.bottom + 24; + + return Positioned( + top: topSectionHeight - 30, + left: 0, + right: 0, + bottom: bottomBarHeight, // Space for bottom action bar + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.only(left: 16, right: 16, top: 50, bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // User name and badges + _buildUserNameSection(), + + const SizedBox(height: 12), + + // Self-description + _buildSelfDescription(), + + const SizedBox(height: 16), + + // Tags/Interests + _buildTagsSection(), + + const SizedBox(height: 16), + + // Location and ID + _buildLocationAndId(), + ], + ), + ), + ), + ); + } + + Widget _buildUserNameSection() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + // User name + Text( + widget.userData.nickName, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + + // Age badge (pink icon with age) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFFFE8F0), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: const Color(0xFFFF69B4), + shape: BoxShape.circle, + ), + child: const Center( + child: Text( + 'Q', + style: TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 4), + Text( + '${widget.userData.age}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFFFF69B4), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + + // Online badge + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF2ED573), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '在线', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + + // Real-name verified badge + if (widget.userData.isRealNameCertified) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFF3E9FF), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + '实名', + style: TextStyle( + fontSize: 12, + color: Color(0xFFA05CFF), + fontWeight: FontWeight.w500, + ), + ), + ), + + // Male gender icon (placeholder) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.imagesManIcon, + width: 16, + height: 16, + ), + const SizedBox(width: 4), + const Text( + '19', + style: TextStyle( + fontSize: 12, + color: Color(0xFF333333), + ), + ), + ], + ), + ), + ], + ), + ), + + // Voice message button with duration + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + // Handle voice message playback + }, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFA05CFF), + shape: BoxShape.circle, + ), + child: Stack( + alignment: Alignment.center, + children: [ + Image.asset( + Assets.imagesPlayIcon, + width: 24, + height: 24, + color: Colors.white, + ), + Image.asset( + Assets.imagesVoiceIcon, + width: 20, + height: 20, + color: Colors.white, + ), + ], + ), + ), + ), + const SizedBox(width: 6), + const Text( + "6'", + style: TextStyle( + fontSize: 14, + color: Color(0xFF333333), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ); + } + + Widget _buildSelfDescription() { + return Text( + widget.userData.describeInfo, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF333333), + ), + ); + } + + Widget _buildTagsSection() { + // 构建标签列表 + final List tags = []; + + // 添加城市 + if (widget.userData.cityName.isNotEmpty) { + tags.add(widget.userData.cityName); + } + if (widget.userData.districtName.isNotEmpty) { + tags.add(widget.userData.districtName); + } + + // 这里可以根据实际数据添加更多标签 + // 例如:身高、学历、兴趣等 + // 由于 MarriageData 模型中没有这些字段,暂时只显示城市信息 + // 如果后续需要更多标签,可以在 MarriageData 中添加相应字段 + + if (tags.isEmpty) { + return const SizedBox.shrink(); + } + + return Wrap( + spacing: 8, + runSpacing: 8, + children: tags.map((tag) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + tag, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF333333), + ), + ), + ); + }).toList(), + ); + } + + Widget _buildLocationAndId() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.userData.provinceName.isNotEmpty) + Text( + 'IP属地: ${widget.userData.provinceName}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 4), + Text( + '动我ID: ${widget.userData.userId}', + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ], + ); + } + + Widget _buildBottomActionBar() { + final bottomPadding = MediaQuery.of(context).padding.bottom; + + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 12, + bottom: bottomPadding + 12, + ), + decoration: BoxDecoration( + color: const Color(0xFF2C2C2C), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + top: false, + child: Row( + children: [ + // Send message button + Expanded( + child: ElevatedButton( + onPressed: () { + // Handle send message + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF3A3A3A), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + elevation: 0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + Assets.imagesTalkIcon, + width: 20, + height: 20, + color: Colors.white, + ), + const SizedBox(width: 8), + const Text( + '发消息', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + + const SizedBox(width: 12), + + // Follow button + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + // Handle follow action + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFA05CFF), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + ), + child: const Text( + '关注', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 5e5e6cc..ac317c7 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -4,8 +4,7 @@ import 'package:dating_touchme_app/pages/mine/mine_page.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:dating_touchme_app/network/user_api.dart'; + import 'package:dating_touchme_app/controller/mine/user_controller.dart'; import '../../widget/double_tap_to_exit_widget.dart'; diff --git a/lib/pages/mine/user_info_page.dart b/lib/pages/mine/user_info_page.dart index ec4bb25..50ed079 100644 --- a/lib/pages/mine/user_info_page.dart +++ b/lib/pages/mine/user_info_page.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:dating_touchme_app/generated/assets.dart'; diff --git a/pubspec.lock b/pubspec.lock index 9d3da77..bb67845 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -241,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + easy_refresh: + dependency: "direct main" + description: + name: easy_refresh + sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" + url: "https://pub.dev" + source: hosted + version: "3.4.0" event_bus: dependency: "direct main" description: @@ -701,6 +709,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index dcf6aee..ce17b03 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: flutter_oss_aliyun: ^6.4.2 permission_handler: ^12.0.1 flustars: ^2.0.1 + easy_refresh: ^3.4.0 dev_dependencies: flutter_test: