flutter/lib/pages/index/index.dart

592 lines
23 KiB
Dart
Raw Permalink Normal View History

2025-09-29 02:34:41 +08:00
/// 首页模板
library;
import 'dart:ui';
import 'package:card_swiper/card_swiper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/im_friend_listeners.dart';
import 'package:loopin/api/shop_api.dart';
import 'package:loopin/components/custom_pageview_indicator.dart';
import 'package:loopin/components/custom_sticky_header.dart';
import 'package:loopin/components/empty_tip.dart';
import 'package:loopin/components/loading.dart';
import 'package:loopin/components/network_or_asset_image.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/styles/index.dart';
import 'package:loopin/utils/index.dart';
2025-10-10 11:27:26 +08:00
import 'package:loopin/utils/network_utils.dart';
2025-09-29 02:34:41 +08:00
import '../../behavior/custom_scroll_behavior.dart';
import '../../components/backtop.dart';
class IndexPage extends StatefulWidget {
const IndexPage({super.key});
@override
State<IndexPage> createState() => _IndexPageState();
}
class _IndexPageState extends State<IndexPage> with TickerProviderStateMixin {
///-----------
// 瀑布流列表
List waterfallData = [
{
'price': 199.00,
'title': '韩料界的萨莉亚!',
'shop': '萨莉亚专卖店',
'image': 'https://qcloud.dpfile.com/pc/1c3egbzM_ICz90dhi6MAiTsazjxWYQcHCd-sbpD1Wqtph2eIJA04NCRvoGqL4_opG45IiB1YIyNuDTtqzVRwesm_qA1Pf8rFcayTY-n-rG8.jpg',
'saleNum': '2.1万'
},
{
'price': 1499.90,
'title': '茅台MOUTAI飞天 53%vol 500ml 贵州茅台酒(带杯)',
'shop': '茅台京东自营旗舰店',
'image': 'https://img13.360buyimg.com/n1/jfs/t1/97097/12/15694/245806/5e7373e6Ec4d1b0ac/9d8c13728cc2544d.jpg',
'saleNum': '1254'
},
{
'price': 18.90,
'title': '上海街头苹果糖!一口一个不吱声',
'shop': '芝洛洛自营旗舰店',
'image': 'https://p0.meituan.net/coverpic/f0eefdfa02619fb09ca53eacd4d97231123115.jpg',
'saleNum': '1.2万'
},
{
'price': 59.00,
'title': '谁懂,就是这个菜,尝了第一口,立马决定加单了,真正的咸甜永动机啊🍬 去过云南的朋友都知道,当地的乳扇真的很好吃。',
'shop': '薄荷牛舌卷旗舰店',
'image': 'https://qcloud.dpfile.com/pc/UcW-v6AN1TxVTt9--5Kaw2-t4W55jUhEG_pM5S-w_AQ4IP3z9WxHzwJ9fOthIjEYY0q73sB2DyQcgmKUxZFQtw.jpg',
'saleNum': '1639'
},
{
'price': 2499.00,
'title': '小米 REDMI K80 国家补贴 第三代骁龙 8 6550mAh大电池 澎湃OS 玄夜黑 12GB+256GB 红米5G至尊手机',
'shop': '小米京东自营旗舰店',
'image': 'https://img10.360buyimg.com/n1/s450x450_jfs/t1/264409/38/13856/102861/678dcfdaFb723c58f/5b97cf154bbba96c.jpg',
'saleNum': '9726'
},
{
'price': 1.00,
'title': '圣菲尔伯爵法国红酒Saintfilcount干红葡萄酒珍藏13.5度单瓶送礼红酒 一元试饮',
'shop': '小森葡萄酒专营店',
'image': 'https://img10.360buyimg.com/n7/jfs/t1/226168/23/3411/118733/65537e5fF2db2d109/7d1d11a8013d6e8f.jpg',
'saleNum': '9.9万'
},
{
'price': 42.00,
'title': '美的MideaLED便携充电小台灯书桌学习阅读灯学生宿舍卧室床头灯学习台灯',
'shop': '美的Midea旗舰店',
'image': 'https://img14.360buyimg.com/mobilecms/s360x360_jfs/t1/226233/4/10194/156936/658e8f88Fcfc9cb40/cea4a48783f11a7a.jpg',
'saleNum': '5106'
},
{
'price': 22.90,
'title': '蒙都 羊杂500g 加热即食 京东超市肉干肉脯及礼包11.11真便宜',
'shop': '蒙都旗舰店',
'image': 'https://img10.360buyimg.com/n7/jfs/t1/155306/32/25324/231912/62d22fb8E4ffab855/c6001ee702fb240a.jpg',
'saleNum': '1.6万'
},
{
'price': 19.90,
'title': '『 江西炒米粉 』本次最佳😋香就一个字话。锅气的香🔥干辣椒的焦香🌶️油的润香🐷蔬菜混合的清香🥬',
'shop': '去月球野餐嗎',
'image': 'https://qcloud.dpfile.com/pc/pOAOL-DQRBWfkVZIWYVoy0mMQf6_UutNlOpEpGkT_nz3b1n7ZbpikPgtXMhMsjXNY0q73sB2DyQcgmKUxZFQtw.jpg',
'saleNum': '3.2万'
},
{
'price': 109.00,
'title': '附近新开业的,作为江西人当然要去试试。点了几个家常菜。',
'shop': '辣评新开江西菜',
'image': 'https://qcloud.dpfile.com/pc/HePD48CFNnS0kMZyf3Q391wxaW_zVgHimctthH__J6UI54HLPUkNt5e3qtP4Nl2G_aW_B6sGElzX-tSmYRvRnQxxxek7cKy7_R0W-KdxWUk.jpg',
'saleNum': '8764'
},
];
// 列表
RxList dataList = [].obs;
// 是否加载中
RxBool isLoading = false.obs;
RxBool isInitLoading = true.obs;
RxBool hasMore = true.obs;
//
RxInt currentIndex = 0.obs;
TextEditingController textEditingController = TextEditingController();
FocusNode focusNode = FocusNode();
TabController? tabController;
// 分类列表
RxList<dynamic> tabList = <dynamic>[].obs;
///轮播图数据
RxList<dynamic> swiperData = <dynamic>[].obs;
late ScrollController scrollController = ScrollController();
final PageController pageController = PageController();
// 滚动位置
double scrollOffset = 0;
int page = 1;
2025-10-10 11:27:26 +08:00
final NetworkUtils networkUtils = NetworkUtils();
2025-09-29 02:34:41 +08:00
/// 初始化 Tab 分类
Future<void> initTabs() async {
page = 1;
currentIndex.value = 0;
isLoading.value = false;
hasMore.value = true;
dataList.value = [];
isInitLoading.value = true;
// 赋值 tab 数据
final res = await Http.post(ShopApi.shopCategory, data: {
'level': 1,
});
final data = res['data'] as List<dynamic>;
tabList.value = data;
// 如果之前有 controller要先释放掉
tabController?.dispose();
tabController = TabController(
initialIndex: 0,
length: tabList.length,
vsync: this,
);
if (tabList.isNotEmpty) {
loadSwiperData();
loadData(currentIndex.value);
}
}
/// 加载swiper数据
Future<void> loadSwiperData() async {
final res = await Http.post(ShopApi.shopSwiperList, data: {
'type': 1,
});
final data = res['data'];
logger.w(res);
swiperData.assignAll(data);
}
/// 切换数据
Future<void> changeData(int index) async {
2025-10-01 12:29:09 +08:00
dataList.clear();
2025-09-29 02:34:41 +08:00
if (isLoading.value) return;
isLoading.value = true;
final res = await Http.post(ShopApi.shopList, data: {
'size': 10,
'current': page,
'categoryId': tabList[index]['id'],
});
final data = res['data']['records'];
final total = res['data']['total'];
logger.w(res);
if (dataList.length >= total) {
hasMore.value = false;
2025-10-01 12:29:09 +08:00
isLoading.value = false;
return;
2025-09-29 02:34:41 +08:00
}
2025-10-01 12:29:09 +08:00
dataList.value = data;
2025-09-29 02:34:41 +08:00
page += 1;
isLoading.value = false;
isInitLoading.value = false;
}
/// 加载pageview数据
Future<void> loadData(int index) async {
if (isLoading.value) return;
isLoading.value = true;
final res = await Http.post(ShopApi.shopList, data: {
'size': 10,
'current': page,
'categoryId': tabList[index]['id'],
});
final data = res['data']['records'];
final total = res['data']['total'];
logger.w(res);
if (dataList.length >= total) {
hasMore.value = false;
2025-10-01 12:29:09 +08:00
isLoading.value = false;
return;
2025-09-29 02:34:41 +08:00
}
2025-10-01 12:29:09 +08:00
dataList.addAll(data);
2025-09-29 02:34:41 +08:00
page += 1;
2025-10-01 12:29:09 +08:00
logger.e(page);
2025-09-29 02:34:41 +08:00
isLoading.value = false;
isInitLoading.value = false;
}
@override
void initState() {
super.initState();
scrollController.addListener(() {
setState(() {
scrollOffset = scrollController.offset;
});
if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
debugPrint('[index]滚动到底部');
if (!isLoading.value) {
loadData(currentIndex.value);
}
}
});
2025-10-10 11:27:26 +08:00
// 监听网络恢复
ever(
networkUtils.isConnected,
(status) {
logger.e('商品首页当前网络状态:$status');
if (status) {
initTabs();
}
},
);
2025-09-29 02:34:41 +08:00
// 初始化加载
initTabs();
}
@override
void dispose() {
scrollController.dispose();
pageController.dispose();
super.dispose();
}
// 瀑布流卡片
Widget cardList(item) {
if (item == null) {
return Loading(
title: '加载中',
);
}
return GestureDetector(
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(5),
offset: Offset(0.0, 1.0),
blurRadius: 1.0,
spreadRadius: 0.0,
),
]),
child: Column(
children: [
NetworkOrAssetImage(
imageUrl: '${item['pic']}',
width: double.infinity,
placeholderAsset: 'assets/images/wait_loading.png',
),
Container(
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 5.0,
children: [
Text(
'${item['name']}',
style: TextStyle(fontSize: 14.0, height: 1.2),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
spacing: 5.0,
children: [
Text.rich(
TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [
TextSpan(text: '¥'),
TextSpan(
text: '${item['price']}',
style: TextStyle(
fontSize: 16.0,
)),
]),
),
Text(
'已售${Utils.graceNumber(int.parse(item['sales'] ?? '0'))}',
style: TextStyle(color: Colors.grey, fontSize: 10.0),
),
],
),
Text(
'${item['storeName']}',
style: TextStyle(color: Colors.grey, fontSize: 12.0),
),
],
),
)
],
),
),
onTap: () {
// Get.toNamed('/goods', arguments: item['id']);
Get.toNamed('/goods', arguments: {'goodsId': item['id']});
},
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
focusNode.unfocus();
},
child: Scaffold(
body: ScrollConfiguration(
behavior: CustomScrollBehavior().copyWith(scrollbars: false),
child: RefreshIndicator(
color: FStyle.primaryColor,
onRefresh: () async {
await initTabs();
},
child: CustomScrollView(
scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false),
controller: scrollController,
slivers: [
SliverAppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
pinned: true,
expandedHeight: 200.0,
titleSpacing: 10.0,
// 搜索框(高斯模糊背景)
title: ClipRRect(
borderRadius: BorderRadius.circular(30.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: Container(
height: 45.0,
decoration: BoxDecoration(
color: Colors.white.withAlpha(150),
),
child: TextField(
focusNode: focusNode,
controller: textEditingController,
decoration: InputDecoration(
isDense: true,
hintText: "热销商品",
hintStyle: TextStyle(fontSize: 15.0),
prefixIcon: Icon(
Icons.search,
color: Colors.black38,
size: 21.0,
),
suffixIcon: Container(
padding: EdgeInsets.only(right: 15.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 10.0,
children: [
TextButton(
onPressed: () {
focusNode.unfocus();
if (textEditingController.text.isNotEmpty) {
// 去搜索结果页支持带着搜索文字和搜索tab索引
Get.toNamed(
'/search-result',
arguments: {'searchWords': textEditingController.text, 'tab': 1},
);
}
},
child: Text('搜索'),
),
],
),
),
contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 10.0),
border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(30.0))),
cursorColor: Colors.black,
onSubmitted: (value) {
focusNode.unfocus();
if (value.isNotEmpty) {
// 去搜索结果页支持带着搜索文字和搜索tab索引
Get.toNamed(
'/search-result',
arguments: {'searchWords': value, 'tab': 1},
);
}
},
),
),
),
),
// 自定义伸缩区域(轮播图)
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFFFF5000), Color(0xFFfcaec4)],
),
),
child: FlexibleSpaceBar(
background: Obx(() {
// 如果数据为空,返回一个空容器或占位图
if (swiperData.isEmpty) return SizedBox.shrink();
return SizedBox(
width: double.infinity,
child: Swiper.children(
pagination: SwiperPagination(
builder: DotSwiperPaginationBuilder(
color: Colors.white70,
activeColor: Colors.white,
size: 6.0,
activeSize: 8.0,
space: 4.0,
),
),
indicatorLayout: PageIndicatorLayout.SCALE,
children: swiperData.map<Widget>((itm) {
return NetworkOrAssetImage(
imageUrl: itm['images'],
placeholderAsset: 'assets/images/bk.jpg',
);
}).toList(),
),
);
}),
),
),
),
// 分类
SliverPersistentHeader(
pinned: false,
delegate: CustomStickyHeader(
child: PreferredSize(
preferredSize: Size.fromHeight(110.0),
child: Obx(
() {
return Container(
margin: EdgeInsets.all(10.0),
padding: EdgeInsets.symmetric(vertical: 10.0),
height: 110.0,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
),
child: Column(
children: [
Expanded(
child: PageView.builder(
controller: pageController,
itemCount: (tabList.length / 4).ceil(),
itemBuilder: (context, pageIndex) {
final start = pageIndex * 4;
final end = (start + 4) > tabList.length ? tabList.length : (start + 4);
final pageItems = tabList.sublist(start, end);
return GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, // 一行
mainAxisSpacing: 0, // 行间距
),
itemCount: pageItems.length,
itemBuilder: (BuildContext context, int index) {
final citem = pageItems[index];
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
//切换index
final globalIndex = start + index;
logger.e(globalIndex);
if (globalIndex != currentIndex.value) {
currentIndex.value = globalIndex;
page = 1;
isLoading.value = false;
changeData(globalIndex);
}
},
child: Column(
spacing: 3.0,
children: [
ClipOval(
child: NetworkOrAssetImage(
imageUrl: citem['icon'],
width: 30.0,
height: 30.0,
placeholderAsset: 'assets/images/wait_loading.png',
),
),
Text(citem['name']),
],
),
);
},
);
},
),
),
// 数量够翻页才显示
CustomPageViewIndicator(
controller: pageController,
count: (tabList.length / 4).ceil(),
color: Color(0xFFCECECE),
activeColor: Color(0xFFFF5000),
),
],
),
);
},
),
),
),
),
// 瀑布流列表
SliverToBoxAdapter(
child: Obx(
() {
2025-10-01 12:29:09 +08:00
// if (isInitLoading.value) {
// return Column(
// children: [
// RefreshProgressIndicator(
// backgroundColor: Colors.white,
// color: Color(0xFFFF5000),
// ),
// ],
// );
// }
2025-09-29 02:34:41 +08:00
if (dataList.isEmpty) {
return EmptyTip();
}
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
children: [
MasonryGridView.count(
shrinkWrap: true,
padding: EdgeInsets.zero,
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
itemCount: dataList.length,
itemBuilder: (BuildContext context, int index) {
return cardList(dataList[index]);
},
),
Opacity(opacity: dataList.isNotEmpty && isLoading.value ? 1 : 0, child: Loading(title: 'loading...')),
],
),
);
},
),
),
],
),
),
),
// 返回顶部
floatingActionButton: Backtop(controller: scrollController, offset: scrollOffset),
),
);
}
}