23 changed files with 1877 additions and 288 deletions
Split View
Diff Options
-
BINassets/images/back_icon.png
-
BINassets/images/information_bg.png
-
BINassets/images/more_icon.png
-
BINassets/images/play_icon.png
-
BINassets/images/talk_icon.png
-
BINassets/images/voice_icon.png
-
302lib/controller/home/home_controller.dart
-
6lib/generated/assets.dart
-
1lib/main.dart
-
87lib/model/home/marriage_data.dart
-
3lib/network/api_urls.dart
-
23lib/network/home_api.dart
-
89lib/network/home_api.g.dart
-
6lib/network/network_service.dart
-
2lib/network/response_model.dart
-
2lib/oss/oss_manager.dart
-
193lib/pages/home/content_card.dart
-
762lib/pages/home/home_page.dart
-
660lib/pages/home/user_information_page.dart
-
3lib/pages/main/main_page.dart
-
1lib/pages/mine/user_info_page.dart
-
24pubspec.lock
-
1pubspec.yaml
@ -0,0 +1,302 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|||
import '../../network/home_api.dart'; |
|||
import '../../model/home/marriage_data.dart'; |
|||
|
|||
class HomeController extends GetxController { |
|||
// 推荐列表数据 |
|||
final recommendFeed = <MarriageData>[].obs; |
|||
// 同城列表数据 |
|||
final nearbyFeed = <MarriageData>[].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<HomeApi>(); |
|||
// 初始化时加载数据 |
|||
loadInitialData(); |
|||
} |
|||
|
|||
/// 加载初始数据(同时加载两个标签页的数据) |
|||
void loadInitialData() async { |
|||
// 并行加载两个标签页的数据 |
|||
await Future.wait([ |
|||
loadRecommendInitialData(), |
|||
loadNearbyInitialData(), |
|||
]); |
|||
} |
|||
|
|||
/// 加载推荐列表初始数据 |
|||
Future<void> loadRecommendInitialData() async { |
|||
if (recommendIsLoading.value) return; |
|||
|
|||
try { |
|||
recommendIsLoading.value = true; |
|||
recommendPage.value = 1; |
|||
recommendHasMore.value = true; |
|||
|
|||
// 获取推荐数据 (type=0) |
|||
final List<MarriageData> 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<void> loadNearbyInitialData() async { |
|||
if (nearbyIsLoading.value) return; |
|||
|
|||
try { |
|||
nearbyIsLoading.value = true; |
|||
nearbyPage.value = 1; |
|||
nearbyHasMore.value = true; |
|||
|
|||
// 获取同城数据 (type=1) |
|||
final List<MarriageData> 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<void> loadMoreData([int? tabIndex]) async { |
|||
final targetTab = tabIndex ?? selectedTabIndex.value; |
|||
|
|||
if (targetTab == 0) { |
|||
// 加载推荐列表更多数据 |
|||
await loadRecommendMoreData(); |
|||
} else { |
|||
// 加载同城列表更多数据 |
|||
await loadNearbyMoreData(); |
|||
} |
|||
} |
|||
|
|||
/// 加载推荐列表更多数据 |
|||
Future<void> loadRecommendMoreData() async { |
|||
if (recommendIsLoading.value || !recommendHasMore.value) return; |
|||
|
|||
try { |
|||
recommendIsLoading.value = true; |
|||
recommendPage.value++; |
|||
|
|||
// 获取推荐数据 (type=0) |
|||
final List<MarriageData> 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<void> loadNearbyMoreData() async { |
|||
if (nearbyIsLoading.value || !nearbyHasMore.value) return; |
|||
|
|||
try { |
|||
nearbyIsLoading.value = true; |
|||
nearbyPage.value++; |
|||
|
|||
// 获取同城数据 (type=1) |
|||
final List<MarriageData> 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<void> refreshData([int? tabIndex]) async { |
|||
final targetTab = tabIndex ?? selectedTabIndex.value; |
|||
|
|||
if (targetTab == 0) { |
|||
// 刷新推荐列表 |
|||
await refreshRecommendData(); |
|||
} else { |
|||
// 刷新同城列表 |
|||
await refreshNearbyData(); |
|||
} |
|||
} |
|||
|
|||
/// 刷新推荐列表数据 |
|||
Future<void> refreshRecommendData() async { |
|||
if (recommendIsLoading.value) return; |
|||
|
|||
try { |
|||
recommendIsLoading.value = true; |
|||
recommendPage.value = 1; |
|||
recommendHasMore.value = true; |
|||
|
|||
// 获取推荐数据 (type=0) |
|||
final List<MarriageData> 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<void> refreshNearbyData() async { |
|||
if (nearbyIsLoading.value) return; |
|||
|
|||
try { |
|||
nearbyIsLoading.value = true; |
|||
nearbyPage.value = 1; |
|||
nearbyHasMore.value = true; |
|||
|
|||
// 获取同城数据 (type=1) |
|||
final List<MarriageData> 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<MarriageData> getFeedListByTab(int tabIndex) { |
|||
return tabIndex == 0 ? List.from(recommendFeed) : List.from(nearbyFeed); |
|||
} |
|||
|
|||
/// 私有方法:获取婚姻数据(统一的数据获取逻辑) |
|||
Future<List<MarriageData>> _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<String, dynamic>)).toList(); |
|||
} else if (data is Map<String, dynamic>) { |
|||
// 如果data是对象,检查是否有list或records字段 |
|||
final listData = data['list'] ?? data['records']; |
|||
if (listData is List) { |
|||
return listData.map((item) => MarriageData.fromJson(item as Map<String, dynamic>)).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); |
|||
} |
|||
} |
|||
@ -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<PhotoItem> photoList; |
|||
|
|||
// 为了兼容UI展示,添加一些计算属性 |
|||
String get name => nickName; |
|||
String get avatar => profilePhoto.trim().replaceAll('`', ''); // 移除照片URL中的反引号 |
|||
String get city => cityName; |
|||
String get description => describeInfo; |
|||
List<String> 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<String, dynamic> 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<dynamic>?)?.map((e) => PhotoItem.fromJson(e as Map<String, dynamic>)).toList() ?? [], |
|||
); |
|||
} |
|||
} |
|||
|
|||
// 照片项数据模型 |
|||
class PhotoItem { |
|||
final String photoUrl; |
|||
final dynamic auditStatus; |
|||
|
|||
PhotoItem({ |
|||
required this.photoUrl, |
|||
this.auditStatus, |
|||
}); |
|||
|
|||
factory PhotoItem.fromJson(Map<String, dynamic> json) { |
|||
return PhotoItem( |
|||
photoUrl: json['photoUrl'] ?? '', |
|||
auditStatus: json['auditStatus'], |
|||
); |
|||
} |
|||
} |
|||
@ -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<HttpResponse<BaseResponse<dynamic>>> getMarriageList({ |
|||
@Query('page') required int page, |
|||
@Query('pageSize') required int pageSize, |
|||
@Query('type') required int type, |
|||
}); |
|||
} |
|||
@ -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<HttpResponse<BaseResponse<dynamic>>> getMarriageList({ |
|||
required int page, |
|||
required int pageSize, |
|||
required int type, |
|||
}) async { |
|||
final _extra = <String, dynamic>{}; |
|||
final queryParameters = <String, dynamic>{ |
|||
r'page': page, |
|||
r'pageSize': pageSize, |
|||
r'type': type, |
|||
}; |
|||
final _headers = <String, dynamic>{}; |
|||
const Map<String, dynamic>? _data = null; |
|||
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>( |
|||
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<Map<String, dynamic>>(_options); |
|||
late BaseResponse<dynamic> _value; |
|||
try { |
|||
_value = BaseResponse<dynamic>.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<T>(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 |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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<UserInformationPage> createState() => _UserInformationPageState(); |
|||
} |
|||
|
|||
class _UserInformationPageState extends State<UserInformationPage> { |
|||
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<String> 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, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save