diff --git a/assets/images/arrow_forward_right.png b/assets/images/arrow_forward_right.png new file mode 100644 index 0000000..b12df97 Binary files /dev/null and b/assets/images/arrow_forward_right.png differ diff --git a/assets/images/example_content.png b/assets/images/example_content.png new file mode 100644 index 0000000..4f450bc Binary files /dev/null and b/assets/images/example_content.png differ diff --git a/lib/controller/mine/user_info_controller.dart b/lib/controller/mine/user_info_controller.dart index 0586d41..358398b 100644 --- a/lib/controller/mine/user_info_controller.dart +++ b/lib/controller/mine/user_info_controller.dart @@ -93,14 +93,14 @@ class UserInfoController extends GetxController { Future handleGallerySelection() async { try { - // 请求相册/照片权限 - final ok = await _ensurePermission( - Permission.photos, - // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 - denyToast: '相册权限被拒绝,请在设置中允许访问相册', + // 请求相册/照片权限 + // final ok = await _ensurePermission( + // Permission.photos, + // // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 + // denyToast: '相册权限被拒绝,请在设置中允许访问相册', - ); - if (!ok) return; + // ); + // if (!ok) return; // 从相册选择图片 final ImagePicker picker = ImagePicker(); diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index b10cf6c..3307706 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -25,5 +25,7 @@ class Assets { static const String imagesSendMessageIcon = 'assets/images/send_message_icon.png'; static const String imagesTabChangeIcon = 'assets/images/tab_change_icon.png'; 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'; } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 8779c16..a786c09 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,5 +1,4 @@ import 'package:dating_touchme_app/generated/assets.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { @@ -12,18 +11,18 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with AutomaticKeepAliveClientMixin { int selectedTabIndex = 0; // 0: 推荐 1: 同城 - // 简单的示例数据结构 + // 简单的示例数据结构,使用更安全的图片数据 final List<_FeedItem> recommendFeed = [ - _FeedItem(type: _FeedType.withImages), - _FeedItem(type: _FeedType.plain), - _FeedItem(type: _FeedType.live), - _FeedItem(type: _FeedType.withImages), + _FeedItem(type: _FeedType.offline, content: '我想找一个有缘的异性,快来联系我吧快来……'), // 不设置图片 + _FeedItem(type: _FeedType.online, content: '我想找一个有缘的异性,快来联系我吧快来……'), + _FeedItem(type: _FeedType.live, content: '正在直播,快来互动~'), + _FeedItem(type: _FeedType.offline, content: '大家好,很高兴认识新朋友!'), // 不设置图片 ]; final List<_FeedItem> nearbyFeed = [ - _FeedItem(type: _FeedType.plain), - _FeedItem(type: _FeedType.withImages), - _FeedItem(type: _FeedType.live), + _FeedItem(type: _FeedType.online, content: '同城的朋友,有空一起出来玩呀~'), + _FeedItem(type: _FeedType.offline, content: '周末有什么好去处推荐吗?'), // 不设置图片 + _FeedItem(type: _FeedType.live, content: '正在直播,快来互动~'), ]; @override @@ -31,33 +30,53 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin 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 Scaffold( - backgroundColor: Colors.white, - appBar: _buildAppBar(), - body: ListView.separated( - physics: const BouncingScrollPhysics(), - itemBuilder: (context, index) { - final item = dataSource[index]; - switch (item.type) { - case _FeedType.withImages: - return ImagesCard(); - case _FeedType.plain: - return PlainCard(); - case _FeedType.live: - return LiveCard(); - } - }, - separatorBuilder: (context, index) => const SizedBox(height: 12), - itemCount: dataSource.length, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - ), - ); + 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 遮挡 + ), + ), + ), + ), + ], + ); } PreferredSizeWidget _buildAppBar() { return AppBar( - backgroundColor: Colors.white, + backgroundColor: Colors.transparent, elevation: 0, centerTitle: true, toolbarHeight: 56, @@ -71,6 +90,16 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin _buildTabButton(title: '同城', index: 1), ], ), + // actions: [ + // Padding( + // padding: const EdgeInsets.only(right: 16), + // child: Image.asset( + // Assets.imagesOnlineIcon, + // width: 20, + // height: 20, + // ), + // ), + // ], bottom: const PreferredSize( preferredSize: Size.fromHeight(4), child: SizedBox(height: 4), @@ -119,32 +148,85 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin bool get wantKeepAlive => true; } -enum _FeedType { withImages, plain, live } +// 卡片类型枚举 +enum _FeedType { + online, // 在线 + HI按钮 + offline, // 下线 + 发消息按钮 + live // 直播 + 直播间按钮(放在HI按钮位置) +} +// 接口数据模型 class _FeedItem { final _FeedType type; - const _FeedItem({required this.type}); + final String? nickname; // 昵称 + final String? avatar; // 头像URL + final int? age; // 年龄 + final String? location; // 地区 + final bool? isVerified; // 是否实名认证 + final String? content; // 内容文本 + final List? images; // 图片列表 + // 从接口数据构造 + const _FeedItem({ + required this.type, + this.nickname = '林园园', + this.avatar, + this.age = 23, + this.location = '白云区', + this.isVerified = true, + this.content = '我想找一个有缘的异性,快来联系我吧快来……', + this.images, + }); + + // 模拟从API响应创建实例的工厂方法 + factory _FeedItem.fromApi(Map apiData) { + // 根据接口参数判断类型 + final bool isLive = apiData['isLive'] ?? false; + final bool isOnline = apiData['isOnline'] ?? false; + + _FeedType type; + if (isLive) { + type = _FeedType.live; + } else if (isOnline) { + type = _FeedType.online; + } else { + type = _FeedType.offline; + } + + return _FeedItem( + type: type, + nickname: apiData['nickname'], + avatar: apiData['avatar'], + age: apiData['age'], + location: apiData['location'], + isVerified: apiData['isVerified'], + content: apiData['content'], + images: List.from(apiData['images'] ?? []), + // viewerCount已移除 + ); + } } // 通用头部组件:头像/昵称/在线/认证/Hi/直播中徽标 + class _CardHeader extends StatelessWidget { - final bool showHi; - // 是否在头像上显示直播态(紫色描边 + 头像角标) - final bool liveOverlayOnAvatar; - const _CardHeader({this.showHi = false, this.liveOverlayOnAvatar = false}); + final _FeedItem 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; + return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // 头像 + 在线小圆点 + // 头像 + 状态小圆点 Stack( children: [ // 头像 + 可选紫色描边 Container( - padding: liveOverlayOnAvatar ? const EdgeInsets.all(2) : EdgeInsets.zero, - decoration: liveOverlayOnAvatar + padding: isLive ? const EdgeInsets.all(2) : EdgeInsets.zero, + decoration: isLive ? BoxDecoration( borderRadius: BorderRadius.circular(32), border: Border.all(color: const Color(0xFFB58BFF), width: 2), @@ -152,45 +234,60 @@ class _CardHeader extends StatelessWidget { : null, child: ClipRRect( borderRadius: BorderRadius.circular(30), - child: Image.asset( - Assets.imagesAvatarsExample, - width: 60, - height: 60, - fit: BoxFit.cover, - ), + child: item.avatar != null + ? Image.network( + item.avatar!, + width: 60, + height: 60, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Image.asset( + Assets.imagesAvatarsExample, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + ) + : Image.asset( + Assets.imagesAvatarsExample, + width: 60, + height: 60, + fit: BoxFit.cover, + ), ), ), + if (isOnline) Positioned( - left: 6, - bottom: 6, + right: 6, + bottom: 1, child: Container( width: 12, height: 12, decoration: BoxDecoration( - color: const Color(0xFF2ED573), + color: isOnline ? const Color(0xFF2ED573) : const Color(0xFFCCCCCC), // 在线绿色,离线灰色 shape: BoxShape.circle, - boxShadow: const [ + boxShadow: isOnline ? const [ BoxShadow(color: Color(0x332ED573), blurRadius: 4, offset: Offset(0, 2)), - ], + ] : [], ), ), ), - if (liveOverlayOnAvatar) + if (isLive) Positioned( - left: -6, - bottom: -6, + bottom: 0, + left: 0, + right: 0, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + width: 37, + height: 14, + alignment: Alignment.center, // 关键:让子内容居中 decoration: BoxDecoration( - color: const Color(0xFF7A5CFF), - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow(color: Color(0x337A5CFF), blurRadius: 6, offset: Offset(0, 3)), - ], + image: DecorationImage( + image: AssetImage(Assets.imagesBtnBgIcon), + ), ), child: const Text( '直播中', - style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600), + style: TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.w600), ), ), ), @@ -203,64 +300,115 @@ class _CardHeader extends StatelessWidget { children: [ Row( children: [ - const Text( - '林园园', - style: TextStyle( + 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), + color: Color.fromRGBO(234, 255, 219, 1) , borderRadius: BorderRadius.circular(12), ), - child: const Text( - '在线', - style: TextStyle(fontSize: 12, color: Color.fromRGBO(38, 199, 124, 1)), + child: Text( + isOnline ? '在线' : '', + style: TextStyle(fontSize: 9, color: Color.fromRGBO(38, 199, 124, 1) ), ), ), const SizedBox(width: 6), - // 实名徽, - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFFF3E9FF), - borderRadius: BorderRadius.circular(12), - ), - 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 (item.isVerified == true) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF3E9FF), + borderRadius: BorderRadius.circular(12), + ), + 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)), + ), + ], + ), ), + 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), - const Text( - '23岁 · 白云区', + Text( + '${item.age}岁 · ${item.location}', style: TextStyle(fontSize: 12, color: Color.fromRGBO(51, 51, 51, 1)), ), ], ), ), - if (showHi) - Image.asset(Assets.imagesHiIcon, width: 40, height: 20), + // 根据不同状态显示不同按钮 - 都放在HI按钮相同位置 + InkWell( + onTap: () { + // 点击事件处理 + if (isLive) { + // 进入直播间逻辑 + print('进入直播间'); + } else if (isOnline) { + // HI按钮点击逻辑 + print('HI点击'); + } else { + // 发消息按钮点击逻辑 + print('发送消息'); + } + }, + child: Image.asset( + _getButtonImage(), + width: item.type == _FeedType.live ? 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按钮 + } + } } -class PlainCard extends StatelessWidget { +// 统一的内容卡片组件,根据接口数据显示不同状态 +class ContentCard extends StatelessWidget { + final _FeedItem item; + const ContentCard({required this.item}); + @override Widget build(BuildContext context) { return Container( @@ -276,133 +424,93 @@ class PlainCard extends StatelessWidget { ], ), padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - _CardHeader(), - SizedBox(height: 8), - Text( - '我想找一个有缘的异性,快来联系我吧快来……', - style: TextStyle(fontSize: 13, color: Color(0xFF666666)), - ), - ], - ), - ); - } -} - -class ImagesCard extends StatelessWidget { - const ImagesCard(); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 12, - offset: Offset(0, 6), - ), - ], - ), - padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _CardHeader(showHi: true), - const SizedBox(height: 10), - const Text( - '我想找一个有缘的异性,快来联系我吧快来……', - style: TextStyle(fontSize: 16, color: Color(0xFF666666)), - ), - const SizedBox(height: 16), - Row( - children: const [ - _BorderedImage(), - SizedBox(width: 12), - _BorderedImage(), - SizedBox(width: 12), - _BorderedImage(), - ], - ), + // 头部:头像、昵称、在线状态、按钮等 - 所有按钮都放在HI位置 + _CardHeader(item: item), + const SizedBox(height: 8), + + // 内容区域 - 根据类型显示不同内容 + _buildContent(), ], ), ); } -} -class LiveCard extends StatelessWidget { - @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: [ - const _CardHeader(liveOverlayOnAvatar: true), - const SizedBox(height: 8), - const Text( - '正在直播,快来互动~', - style: TextStyle(fontSize: 13, color: Color(0xFF666666)), + Widget _buildContent() { + final List contentWidgets = []; + + // 内容文本 + if (item.content != null && item.content!.isNotEmpty) { + contentWidgets.add( + Text( + item.content!, + style: TextStyle( + fontSize: 13, + color: Color.fromRGBO(51, 51, 51, 0.6), ), - const SizedBox(height: 10), - Align( - alignment: Alignment.centerLeft, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF2D7CFF), Color(0xFF7A5CFF)], + ), + ); + } + + // 根据接口返回的图片列表动态显示图片 + 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), ), - borderRadius: BorderRadius.circular(18), - ), - child: const Text( - '进入直播间', - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600), - ), - ), + ); + }).toList(), ), - ], - ), + ); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: contentWidgets, ); } } // 占位组件已被带描边版本替代,保留空实现会触发未使用告警,故移除 -// 带蓝色描边的图片占位 -class _BorderedImage extends StatelessWidget { - const _BorderedImage(); +// 网络图片组件,支持加载网络图片并显示占位图 - 修复可能的闪退问题 +class _NetworkImageWidget extends StatelessWidget { + final String imageUrl; + final double aspectRatio; + const _NetworkImageWidget({required this.imageUrl, required this.aspectRatio}); @override Widget build(BuildContext context) { - return Expanded( - child: AspectRatio( - aspectRatio: 1.05, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFF5D7BFF), width: 2), - ), - clipBehavior: Clip.antiAlias, - child: Image.asset( - Assets.imagesDiscoverPre, - fit: BoxFit.cover, - ), + return AspectRatio( + aspectRatio: aspectRatio, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + ), + clipBehavior: Clip.antiAlias, + child: Image.asset( + // 暂时使用本地图片替代网络图片,避免网络加载失败导致闪退 + Assets.imagesExampleContent, + fit: BoxFit.cover, ), ), ); diff --git a/lib/pages/mine/user_info_page.dart b/lib/pages/mine/user_info_page.dart index 2466885..ec4bb25 100644 --- a/lib/pages/mine/user_info_page.dart +++ b/lib/pages/mine/user_info_page.dart @@ -270,14 +270,7 @@ class UserInfoPage extends StatelessWidget { color: Colors.grey, ), child: ClipOval( - child: controller.avatarLocalPath.value.isNotEmpty - ? Image.file( - File(controller.avatarLocalPath.value), - fit: BoxFit.cover, - width: 85, - height: 85, - ) - : (controller.avatarUrl.value.startsWith('http') + child: (controller.avatarUrl.value.startsWith('http') ? Image.network( controller.avatarUrl.value, fit: BoxFit.cover, @@ -451,7 +444,11 @@ class UserInfoPage extends StatelessWidget { color: controller.birthday.value.isEmpty ? Colors.grey : Colors.black, ), ), - const Icon(Icons.arrow_forward_ios, color: Colors.grey), + Image.asset( + Assets.imagesArrowForwardRight, + width: 5, + height: 10, + ), ], ), ), @@ -491,7 +488,11 @@ class UserInfoPage extends StatelessWidget { color: controller.education.value.isEmpty ? Colors.grey : Colors.black, ), ), - const Icon(Icons.arrow_forward_ios, color: Colors.grey), + Image.asset( + Assets.imagesArrowForwardRight, + width: 5, + height: 10, + ), ], ), ),