flutter/lib/pages/index/index.dart
2025-10-10 11:44:34 +08:00

592 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 首页模板
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';
import 'package:loopin/utils/network_utils.dart';
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;
final NetworkUtils networkUtils = NetworkUtils();
/// 初始化 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 {
dataList.clear();
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;
isLoading.value = false;
return;
}
dataList.value = data;
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;
isLoading.value = false;
return;
}
dataList.addAll(data);
page += 1;
logger.e(page);
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);
}
}
});
// 监听网络恢复
ever(
networkUtils.isConnected,
(status) {
logger.e('商品首页当前网络状态:$status');
if (status) {
initTabs();
}
},
);
// 初始化加载
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(
() {
// if (isInitLoading.value) {
// return Column(
// children: [
// RefreshProgressIndicator(
// backgroundColor: Colors.white,
// color: Color(0xFFFF5000),
// ),
// ],
// );
// }
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),
),
);
}
}