Browse Source

开发首页

ios
Jolie 4 months ago
parent
commit
74f5abdabc
6 changed files with 326 additions and 215 deletions
  1. BIN
      assets/images/arrow_forward_right.png
  2. BIN
      assets/images/example_content.png
  3. 14
      lib/controller/mine/user_info_controller.dart
  4. 2
      lib/generated/assets.dart
  5. 504
      lib/pages/home/home_page.dart
  6. 21
      lib/pages/mine/user_info_page.dart

BIN
assets/images/arrow_forward_right.png

Before After
Width: 28  |  Height: 50  |  Size: 389 B

BIN
assets/images/example_content.png

Before After
Width: 330  |  Height: 330  |  Size: 214 KiB

14
lib/controller/mine/user_info_controller.dart

@ -93,14 +93,14 @@ class UserInfoController extends GetxController {
Future<void> handleGallerySelection() async { Future<void> handleGallerySelection() async {
try { try {
// /
final ok = await _ensurePermission(
Permission.photos,
// Android photos storage/mediaLibrarypermission_handler
denyToast: '相册权限被拒绝,请在设置中允许访问相册',
// /
// final ok = await _ensurePermission(
// Permission.photos,
// // Android photos storage/mediaLibrarypermission_handler
// denyToast: '相册权限被拒绝,请在设置中允许访问相册',
);
if (!ok) return;
// );
// if (!ok) return;
// //
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();

2
lib/generated/assets.dart

@ -25,5 +25,7 @@ class Assets {
static const String imagesSendMessageIcon = 'assets/images/send_message_icon.png'; static const String imagesSendMessageIcon = 'assets/images/send_message_icon.png';
static const String imagesTabChangeIcon = 'assets/images/tab_change_icon.png'; static const String imagesTabChangeIcon = 'assets/images/tab_change_icon.png';
static const String imagesVerifiedIcon = 'assets/images/verified_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';
} }

504
lib/pages/home/home_page.dart

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

21
lib/pages/mine/user_info_page.dart

@ -270,14 +270,7 @@ class UserInfoPage extends StatelessWidget {
color: Colors.grey, color: Colors.grey,
), ),
child: ClipOval( 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( ? Image.network(
controller.avatarUrl.value, controller.avatarUrl.value,
fit: BoxFit.cover, fit: BoxFit.cover,
@ -451,7 +444,11 @@ class UserInfoPage extends StatelessWidget {
color: controller.birthday.value.isEmpty ? Colors.grey : Colors.black, 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, 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,
),
], ],
), ),
), ),

Loading…
Cancel
Save