Browse Source

开发首页

ios
Jolie 4 months ago
parent
commit
535bf1ef8e
23 changed files with 1877 additions and 288 deletions
  1. BIN
      assets/images/back_icon.png
  2. BIN
      assets/images/information_bg.png
  3. BIN
      assets/images/more_icon.png
  4. BIN
      assets/images/play_icon.png
  5. BIN
      assets/images/talk_icon.png
  6. BIN
      assets/images/voice_icon.png
  7. 302
      lib/controller/home/home_controller.dart
  8. 6
      lib/generated/assets.dart
  9. 1
      lib/main.dart
  10. 87
      lib/model/home/marriage_data.dart
  11. 3
      lib/network/api_urls.dart
  12. 23
      lib/network/home_api.dart
  13. 89
      lib/network/home_api.g.dart
  14. 6
      lib/network/network_service.dart
  15. 2
      lib/network/response_model.dart
  16. 2
      lib/oss/oss_manager.dart
  17. 193
      lib/pages/home/content_card.dart
  18. 762
      lib/pages/home/home_page.dart
  19. 660
      lib/pages/home/user_information_page.dart
  20. 3
      lib/pages/main/main_page.dart
  21. 1
      lib/pages/mine/user_info_page.dart
  22. 24
      pubspec.lock
  23. 1
      pubspec.yaml

BIN
assets/images/back_icon.png

Before After
Width: 124  |  Height: 124  |  Size: 3.1 KiB

BIN
assets/images/information_bg.png

Before After
Width: 1500  |  Height: 1539  |  Size: 12 KiB

BIN
assets/images/more_icon.png

Before After
Width: 124  |  Height: 124  |  Size: 3.4 KiB

BIN
assets/images/play_icon.png

Before After
Width: 62  |  Height: 62  |  Size: 1.5 KiB

BIN
assets/images/talk_icon.png

Before After
Width: 53  |  Height: 48  |  Size: 613 B

BIN
assets/images/voice_icon.png

Before After
Width: 67  |  Height: 60  |  Size: 561 B

302
lib/controller/home/home_controller.dart

@ -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);
}
}

6
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';
}

1
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(

87
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<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'],
);
}
}

3
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端点
}

23
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<HttpResponse<BaseResponse<dynamic>>> getMarriageList({
@Query('page') required int page,
@Query('pageSize') required int pageSize,
@Query('type') required int type,
});
}

89
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<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

6
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显示加载指示器

2
lib/network/response_model.dart

@ -77,7 +77,7 @@ class PaginatedResponse<T> {
Map<String, dynamic> 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,

2
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';

193
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();
}
}

762
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<HomePage> createState() => _HomePageState();
}
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> 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<HomeController>(
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<HomePage> 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<HomePage> 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<HomePage> with AutomaticKeepAliveClientMixin
);
}
// WidgetHomePage更简洁
//
Widget _buildRecommendList(HomeController controller, double totalBottomPadding) {
return Obx(() {
final List<MarriageData> 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<MarriageData> 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<String>? 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<String, dynamic> 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<String>.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<Widget> 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<Widget> 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 (2312)
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
// 使123使
// 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,
);
}
}

660
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<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,
),
),
),
),
],
),
),
),
);
}
}

3
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';

1
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';

24
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:

1
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:

Loading…
Cancel
Save