23 changed files with 1877 additions and 288 deletions
Unified 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