|
|
@ -1,5 +1,4 @@ |
|
|
import 'package:dating_touchme_app/generated/assets.dart'; |
|
|
import 'package:dating_touchme_app/generated/assets.dart'; |
|
|
import 'package:flutter/cupertino.dart'; |
|
|
|
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
|
|
|
|
|
|
class HomePage extends StatefulWidget { |
|
|
class HomePage extends StatefulWidget { |
|
|
@ -12,18 +11,18 @@ class HomePage extends StatefulWidget { |
|
|
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin { |
|
|
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin { |
|
|
int selectedTabIndex = 0; // 0: 推荐 1: 同城 |
|
|
int selectedTabIndex = 0; // 0: 推荐 1: 同城 |
|
|
|
|
|
|
|
|
// 简单的示例数据结构 |
|
|
|
|
|
|
|
|
// 简单的示例数据结构,使用更安全的图片数据 |
|
|
final List<_FeedItem> recommendFeed = [ |
|
|
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 = [ |
|
|
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 |
|
|
@override |
|
|
@ -31,33 +30,53 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin |
|
|
super.build(context); |
|
|
super.build(context); |
|
|
final List<_FeedItem> dataSource = |
|
|
final List<_FeedItem> dataSource = |
|
|
selectedTabIndex == 0 ? recommendFeed : nearbyFeed; |
|
|
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() { |
|
|
PreferredSizeWidget _buildAppBar() { |
|
|
return AppBar( |
|
|
return AppBar( |
|
|
backgroundColor: Colors.white, |
|
|
|
|
|
|
|
|
backgroundColor: Colors.transparent, |
|
|
elevation: 0, |
|
|
elevation: 0, |
|
|
centerTitle: true, |
|
|
centerTitle: true, |
|
|
toolbarHeight: 56, |
|
|
toolbarHeight: 56, |
|
|
@ -71,6 +90,16 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin |
|
|
_buildTabButton(title: '同城', index: 1), |
|
|
_buildTabButton(title: '同城', index: 1), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
|
|
|
// actions: [ |
|
|
|
|
|
// Padding( |
|
|
|
|
|
// padding: const EdgeInsets.only(right: 16), |
|
|
|
|
|
// child: Image.asset( |
|
|
|
|
|
// Assets.imagesOnlineIcon, |
|
|
|
|
|
// width: 20, |
|
|
|
|
|
// height: 20, |
|
|
|
|
|
// ), |
|
|
|
|
|
// ), |
|
|
|
|
|
// ], |
|
|
bottom: const PreferredSize( |
|
|
bottom: const PreferredSize( |
|
|
preferredSize: Size.fromHeight(4), |
|
|
preferredSize: Size.fromHeight(4), |
|
|
child: SizedBox(height: 4), |
|
|
child: SizedBox(height: 4), |
|
|
@ -119,32 +148,85 @@ class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin |
|
|
bool get wantKeepAlive => true; |
|
|
bool get wantKeepAlive => true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
enum _FeedType { withImages, plain, live } |
|
|
|
|
|
|
|
|
// 卡片类型枚举 |
|
|
|
|
|
enum _FeedType { |
|
|
|
|
|
online, // 在线 + HI按钮 |
|
|
|
|
|
offline, // 下线 + 发消息按钮 |
|
|
|
|
|
live // 直播 + 直播间按钮(放在HI按钮位置) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 接口数据模型 |
|
|
class _FeedItem { |
|
|
class _FeedItem { |
|
|
final _FeedType type; |
|
|
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<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/直播中徽标 |
|
|
// 通用头部组件:头像/昵称/在线/认证/Hi/直播中徽标 |
|
|
|
|
|
|
|
|
class _CardHeader extends StatelessWidget { |
|
|
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 |
|
|
@override |
|
|
Widget build(BuildContext context) { |
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
final bool isLive = item.type == _FeedType.live; |
|
|
|
|
|
final bool isOnline = item.type == _FeedType.online; |
|
|
|
|
|
|
|
|
return Row( |
|
|
return Row( |
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
crossAxisAlignment: CrossAxisAlignment.center, |
|
|
children: [ |
|
|
children: [ |
|
|
// 头像 + 在线小圆点 |
|
|
|
|
|
|
|
|
// 头像 + 状态小圆点 |
|
|
Stack( |
|
|
Stack( |
|
|
children: [ |
|
|
children: [ |
|
|
// 头像 + 可选紫色描边 |
|
|
// 头像 + 可选紫色描边 |
|
|
Container( |
|
|
Container( |
|
|
padding: liveOverlayOnAvatar ? const EdgeInsets.all(2) : EdgeInsets.zero, |
|
|
|
|
|
decoration: liveOverlayOnAvatar |
|
|
|
|
|
|
|
|
padding: isLive ? const EdgeInsets.all(2) : EdgeInsets.zero, |
|
|
|
|
|
decoration: isLive |
|
|
? BoxDecoration( |
|
|
? BoxDecoration( |
|
|
borderRadius: BorderRadius.circular(32), |
|
|
borderRadius: BorderRadius.circular(32), |
|
|
border: Border.all(color: const Color(0xFFB58BFF), width: 2), |
|
|
border: Border.all(color: const Color(0xFFB58BFF), width: 2), |
|
|
@ -152,45 +234,60 @@ class _CardHeader extends StatelessWidget { |
|
|
: null, |
|
|
: null, |
|
|
child: ClipRRect( |
|
|
child: ClipRRect( |
|
|
borderRadius: BorderRadius.circular(30), |
|
|
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( |
|
|
Positioned( |
|
|
left: 6, |
|
|
|
|
|
bottom: 6, |
|
|
|
|
|
|
|
|
right: 6, |
|
|
|
|
|
bottom: 1, |
|
|
child: Container( |
|
|
child: Container( |
|
|
width: 12, |
|
|
width: 12, |
|
|
height: 12, |
|
|
height: 12, |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: const Color(0xFF2ED573), |
|
|
|
|
|
|
|
|
color: isOnline ? const Color(0xFF2ED573) : const Color(0xFFCCCCCC), // 在线绿色,离线灰色 |
|
|
shape: BoxShape.circle, |
|
|
shape: BoxShape.circle, |
|
|
boxShadow: const [ |
|
|
|
|
|
|
|
|
boxShadow: isOnline ? const [ |
|
|
BoxShadow(color: Color(0x332ED573), blurRadius: 4, offset: Offset(0, 2)), |
|
|
BoxShadow(color: Color(0x332ED573), blurRadius: 4, offset: Offset(0, 2)), |
|
|
], |
|
|
|
|
|
|
|
|
] : [], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
if (liveOverlayOnAvatar) |
|
|
|
|
|
|
|
|
if (isLive) |
|
|
Positioned( |
|
|
Positioned( |
|
|
left: -6, |
|
|
|
|
|
bottom: -6, |
|
|
|
|
|
|
|
|
bottom: 0, |
|
|
|
|
|
left: 0, |
|
|
|
|
|
right: 0, |
|
|
child: Container( |
|
|
child: Container( |
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), |
|
|
|
|
|
|
|
|
width: 37, |
|
|
|
|
|
height: 14, |
|
|
|
|
|
alignment: Alignment.center, // 关键:让子内容居中 |
|
|
decoration: BoxDecoration( |
|
|
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( |
|
|
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: [ |
|
|
children: [ |
|
|
Row( |
|
|
Row( |
|
|
children: [ |
|
|
children: [ |
|
|
const Text( |
|
|
|
|
|
'林园园', |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
|
|
|
Text( |
|
|
|
|
|
item.nickname ?? '用户', |
|
|
|
|
|
style: const TextStyle( |
|
|
fontSize: 15, |
|
|
fontSize: 15, |
|
|
fontWeight: FontWeight.w500, |
|
|
fontWeight: FontWeight.w500, |
|
|
color: Color.fromRGBO(51, 51, 51, 1), |
|
|
color: Color.fromRGBO(51, 51, 51, 1), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
const SizedBox(width: 6), |
|
|
const SizedBox(width: 6), |
|
|
// 在线徽标 |
|
|
|
|
|
|
|
|
// 在线/离线徽标 |
|
|
|
|
|
if (isOnline == true) |
|
|
Container( |
|
|
Container( |
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), |
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: Color.fromRGBO(234, 255, 219, 1), |
|
|
|
|
|
|
|
|
color: Color.fromRGBO(234, 255, 219, 1) , |
|
|
borderRadius: BorderRadius.circular(12), |
|
|
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), |
|
|
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 SizedBox(height: 4), |
|
|
const Text( |
|
|
|
|
|
'23岁 · 白云区', |
|
|
|
|
|
|
|
|
Text( |
|
|
|
|
|
'${item.age}岁 · ${item.location}', |
|
|
style: TextStyle(fontSize: 12, color: Color.fromRGBO(51, 51, 51, 1)), |
|
|
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 |
|
|
@override |
|
|
Widget build(BuildContext context) { |
|
|
Widget build(BuildContext context) { |
|
|
return Container( |
|
|
return Container( |
|
|
@ -276,133 +424,93 @@ class PlainCard extends StatelessWidget { |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
padding: const EdgeInsets.all(12), |
|
|
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( |
|
|
child: Column( |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
children: [ |
|
|
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<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), |
|
|
), |
|
|
), |
|
|
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 |
|
|
@override |
|
|
Widget build(BuildContext context) { |
|
|
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, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
); |
|
|
); |
|
|
|