/// 首页模板 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 '../../behavior/custom_scroll_behavior.dart'; import '../../components/backtop.dart'; class IndexPage extends StatefulWidget { const IndexPage({super.key}); @override State createState() => _IndexPageState(); } class _IndexPageState extends State 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': '美的(Midea)LED便携充电小台灯书桌学习阅读灯学生宿舍卧室床头灯学习台灯', '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 tabList = [].obs; ///轮播图数据 RxList swiperData = [].obs; late ScrollController scrollController = ScrollController(); final PageController pageController = PageController(); // 滚动位置 double scrollOffset = 0; int page = 1; /// 初始化 Tab 分类 Future 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; tabList.value = data; // 如果之前有 controller,要先释放掉 tabController?.dispose(); tabController = TabController( initialIndex: 0, length: tabList.length, vsync: this, ); if (tabList.isNotEmpty) { loadSwiperData(); loadData(currentIndex.value); } } /// 加载swiper数据 Future loadSwiperData() async { final res = await Http.post(ShopApi.shopSwiperList, data: { 'type': 1, }); final data = res['data']; logger.w(res); swiperData.assignAll(data); } /// 切换数据 Future changeData(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); dataList.value = data; if (dataList.length >= total) { hasMore.value = false; } // logger.w(res); page += 1; isLoading.value = false; isInitLoading.value = false; } /// 加载pageview数据 Future 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); dataList.addAll(data); if (dataList.length >= total) { hasMore.value = false; } // logger.w(res); page += 1; 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); } } }); // 初始化加载 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((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), ), ); } }