From 87e890f97e950ea1c2a8551841c5eb07e1707433 Mon Sep 17 00:00:00 2001 From: userGyl Date: Fri, 6 Feb 2026 17:37:43 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=AF=E5=88=86=E5=85=91=E6=8D=A2=E9=A6=96?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ln_jq_app/assets/images/mall_bar@2x.png | Bin 0 -> 896 bytes .../pages/c_page/mall/mall_controller.dart | 148 +++++++- .../lib/pages/c_page/mall/mall_view.dart | 330 +++++++++++++++++- 3 files changed, 460 insertions(+), 18 deletions(-) create mode 100644 ln_jq_app/assets/images/mall_bar@2x.png diff --git a/ln_jq_app/assets/images/mall_bar@2x.png b/ln_jq_app/assets/images/mall_bar@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..651cb201c098e5782b167095b657a46324bc72eb GIT binary patch literal 896 zcmV-`1AqL9P)SlFV+oBIAzG;zDky-)${(;Jn1GGiaj+;* zh|$XU$0l51jE1uik}HsahzaJ#H!tkDx3}x=X{_?7V$@3fPS3s9F}9 zR9hocfzj|(^*jgWh}j6mnpQ8`q!XqrBB5t$G*!gr8Dql;80B8&9Jzi}5ewN!n0}ZI zp$sD7sX#wd*eD>B!Oo3x;Dnkc1HI-HX(C}Uf!EEGJ1Hv+d&1L!dqxgHB*gmx_=I8H zwJYKcY6Z$Ci12>sAtfK+tuS22H-{ps0k9nn30oXxr9}`?6sAPB7+~9$AwpC^hOX5Z zs(lJ$!4IBbW>>to#H>2X44A3_f(jl+WI6zDu!iZXz5~t2uUO4?dbwy;yP55<4a9q) z!5@Oqkrvl*QbgKLb^_fdD{6dowuzGK3vf%nDXLO+;FPV8VU!~B`12riziG=teCfRr zPxR4H?F3YmoDjD8l~3^C<88QH*(r3VFDW8xzgHpoYcUICHxjhJJP+E}N6=M$*D%4% zHw})KR;G13L5c_ku-=A-!yz#54k9|vi2yM06&?bwLyCy6&=1rn=HSi8u0rv?{bo~I z_7(WkvYQ}9#4T7Z^Zzdq>W6DpePF=pw#ZgzOBIPPy#oWX37!0jEhvv^f6#@8g{GBtK8+YQ@^dDzv|9f|loJ#5HL{*1SfVh%8iH#C55RmQt}DL zAg1gofEYh0mmz;6F&@+(DzSQf838ZtK99|e40}nUW(6&%$N1ZPKN)rtDNcf_K{*8x z5;&O-{anB!y+p$YA* zJ~G}oTS0^vL*B^fxV_2W8!)jMt99ikR%qw+n04JB00030|5n`gE&u=k21!IgR09B? WS%da;ToVES0000 json) { + return GoodsModel( + id: json['id'] as int, + categoryId: json['categoryId']?.toString() ?? '', + goodsName: json['goodsName']?.toString() ?? '', + goodsImage: json['goodsImage'], + originalScore: json['originalScore'] as int? ?? 0, + score: json['score'] as int? ?? 0, + stock: json['stock'] as int? ?? 0, + status: json['status'] as int? ?? 1, + ); + } +} + +class UserScore { + final int score; + final int todaySign; + + UserScore({required this.score, required this.todaySign}); + + factory UserScore.fromJson(Map json) { + return UserScore( + score: json['score'] as int? ?? 0, + todaySign: json['todaySign'] as int? ?? 1, + ); + } +} class MallController extends GetxController with BaseControllerMixin { - MallController(); - @override String get builderId => 'mall'; - @override - bool get listenLifecycleEvent => true; + final RxInt userScore = 0.obs; + final RxInt todaySign = 1.obs; // 0可签到,1已签到 + final RxList goodsList = [].obs; + final RxBool isLoading = true.obs; @override void onInit() { super.onInit(); + refreshData(); + } + + Future refreshData() async { + isLoading.value = true; + await Future.wait([getUserScore(), getGoodsList()]); + isLoading.value = false; + updateUi(); + } + + /// 获取用户积分和签到状态 + Future getUserScore() async { + try { + var response = await HttpService.to.post('appointment/score/getUserScore'); + if (response != null && response.data != null) { + var result = BaseModel.fromJson( + response.data, + dataBuilder: (dataJson) => UserScore.fromJson(dataJson), + ); + if (result.code == 0 && result.data != null) { + userScore.value = result.data!.score; + todaySign.value = result.data!.todaySign; + } + } + } catch (e) { + log('获取积分失败: $e'); + } + } + + /// 签到逻辑 + Future signAction() async { + if (todaySign.value == 1) return; + + showLoading('签到中...'); + try { + var response = await HttpService.to.post('appointment/score/sign'); + dismissLoading(); + if (response != null && response.data != null) { + var result = BaseModel.fromJson(response.data); + if (result.code == 0) { + showSuccessToast('签到成功'); + getUserScore(); // 签到成功后刷新积分 + } else { + showErrorToast(result.message); + } + } + } catch (e) { + dismissLoading(); + showErrorToast('签到异常'); + } + } + + /// 获取商品列表 + Future getGoodsList() async { + try { + var response = await HttpService.to.post( + 'appointment/score/getScoreGoodsList', + data: {'categoryId': 0}, + ); + if (response != null && response.data != null) { + var result = BaseModel>.fromJson( + response.data, + dataBuilder: (dataJson) { + var list = dataJson as List; + return list.map((e) => GoodsModel.fromJson(e)).toList(); + }, + ); + if (result.code == 0 && result.data != null) { + goodsList.assignAll(result.data!); + } + } + } catch (e) { + log('获取商品列表失败: $e'); + } + } + + /// 兑换商品 (预留) + void exchangeGoods(GoodsModel goods) { + if (userScore.value < goods.score) { + showWarningToast('积分不足'); + return; + } + + //todo 跳转 } } diff --git a/ln_jq_app/lib/pages/c_page/mall/mall_view.dart b/ln_jq_app/lib/pages/c_page/mall/mall_view.dart index a7772ba..992d0fc 100644 --- a/ln_jq_app/lib/pages/c_page/mall/mall_view.dart +++ b/ln_jq_app/lib/pages/c_page/mall/mall_view.dart @@ -1,24 +1,338 @@ import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:getx_scaffold/getx_scaffold.dart'; +import 'package:ln_jq_app/common/login_util.dart'; import 'mall_controller.dart'; class MallPage extends GetView { const MallPage({super.key}); - Widget _buildView() { - return Stack(children: []); - } - @override Widget build(BuildContext context) { - return GetBuilder( + return GetX( init: MallController(), - id: 'mall', builder: (_) { - return _buildView(); + return Scaffold( + backgroundColor: Color.fromRGBO(247, 249, 251, 1), + body: RefreshIndicator( + onRefresh: controller.refreshData, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Container( + padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 20.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Column(children: [_buildAppBar(), _buildScoreCard()]), + ), + ), + _buildSectionHeader(), + _buildGoodsGrid(), + SliverToBoxAdapter(child: SizedBox(height: 120.h)), + ], + ), + ), + ); }, ); } + + Widget _buildAppBar() { + return Container( + padding: EdgeInsets.only(top: MediaQuery.of(Get.context!).padding.top, bottom: 0), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + borderRadius: BorderRadius.circular(8), + ), + child: LoginUtil.getAssImg("mall_bar@2x"), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '氢能商城', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF333333), + ), + ), + Row( + children: [ + const Text( + 'Hydrogen Energy Mall', + style: TextStyle(fontSize: 12, color: Color(0xFF999999)), + ), + ], + ), + ], + ), + ), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.notifications_none, color: Color(0xFF333333)), + ), + ], + ), + ); + } + + Widget _buildScoreCard() { + return Container( + margin: EdgeInsets.only(top: 22.h), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color.fromRGBO(2, 27, 31, 1), Color.fromRGBO(11, 67, 67, 1)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '我的可用积分', + style: TextStyle(color: Colors.white70, fontSize: 14.sp), + ), + const SizedBox(width: 4), + const Icon(Icons.help_outline, color: Colors.white70, size: 14), + ], + ), + Text( + 'Available points', + style: TextStyle(color: Colors.white38, fontSize: 11.sp), + ), + ], + ), + GestureDetector( + onTap: controller.signAction, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: controller.todaySign.value == 0 + ? Color.fromRGBO(78, 184, 49, 1) + : Colors.grey, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + controller.todaySign.value == 0 ? '立即签到' : '已签到', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + controller.userScore.value.toString(), + style: TextStyle( + color: Color.fromRGBO(236, 236, 236, 1), + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + TextButton( + onPressed: () {}, + child: Text( + '历史订单', + style: TextStyle( + color: Color.fromRGBO(148, 163, 184, 1), + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSectionHeader() { + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + Container( + width: 4.w, + height: 16.h, + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 8), + Text( + '热门商品', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Color.fromRGBO(78, 89, 105, 1), + ), + ), + ], + ), + ), + ); + } + + Widget _buildGoodsGrid() { + if (controller.goodsList.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 50), + child: Text('暂无商品', style: TextStyle(color: Color(0xFF999999))), + ), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 14.h, + crossAxisSpacing: 19.w, + childAspectRatio: 0.75, + ), + delegate: SliverChildBuilderDelegate((context, index) { + final goods = controller.goodsList[index]; + return _buildGoodsItem(goods); + }, childCount: controller.goodsList.length), + ), + ); + } + + Widget _buildGoodsItem(GoodsModel goods) { + return Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: goods.goodsImage != null + ? Image.network( + goods.goodsImage!, + fit: BoxFit.cover, + width: double.infinity, + ) + : Container( + color: const Color(0xFFEEEEEE), + child: const Center( + child: Icon(Icons.image, color: Colors.grey, size: 40), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + goods.goodsName, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + '${goods.score}', + style: TextStyle( + fontSize: 16.sp, + color: Color(0xFF4CAF50), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 4), + Text( + '积分', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.bold, + color: Color(0xFF4CAF50), + ), + ), + ], + ), + GestureDetector( + onTap: () => controller.exchangeGoods(goods), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFFE8F5E9), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '兑换', + style: TextStyle( + color: Color(0xFF4CAF50), + fontWeight: FontWeight.bold, + fontSize: 13.sp, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } }