积分兑换首页

This commit is contained in:
2026-02-06 17:37:43 +08:00
parent dcf925b8c1
commit 87e890f97e
3 changed files with 460 additions and 18 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -1,22 +1,150 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:geolocator/geolocator.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:ln_jq_app/common/model/base_model.dart';
class GoodsModel {
final int id;
final String categoryId;
final String goodsName;
final String? goodsImage;
final int originalScore;
final int score;
final int stock;
final int status;
GoodsModel({
required this.id,
required this.categoryId,
required this.goodsName,
this.goodsImage,
required this.originalScore,
required this.score,
required this.stock,
required this.status,
});
factory GoodsModel.fromJson(Map<String, dynamic> 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<String, dynamic> json) {
return UserScore(
score: json['score'] as int? ?? 0,
todaySign: json['todaySign'] as int? ?? 1,
);
}
}
class MallController extends GetxController with BaseControllerMixin { class MallController extends GetxController with BaseControllerMixin {
MallController();
@override @override
String get builderId => 'mall'; String get builderId => 'mall';
@override final RxInt userScore = 0.obs;
bool get listenLifecycleEvent => true; final RxInt todaySign = 1.obs; // 0可签到1已签到
final RxList<GoodsModel> goodsList = <GoodsModel>[].obs;
final RxBool isLoading = true.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([getUserScore(), getGoodsList()]);
isLoading.value = false;
updateUi();
}
/// 获取用户积分和签到状态
Future<void> getUserScore() async {
try {
var response = await HttpService.to.post('appointment/score/getUserScore');
if (response != null && response.data != null) {
var result = BaseModel<UserScore>.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<void> 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<void> getGoodsList() async {
try {
var response = await HttpService.to.post(
'appointment/score/getScoreGoodsList',
data: {'categoryId': 0},
);
if (response != null && response.data != null) {
var result = BaseModel<List<GoodsModel>>.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 跳转
} }
} }

View File

@@ -1,24 +1,338 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:getx_scaffold/getx_scaffold.dart'; import 'package:getx_scaffold/getx_scaffold.dart';
import 'package:ln_jq_app/common/login_util.dart';
import 'mall_controller.dart'; import 'mall_controller.dart';
class MallPage extends GetView<MallController> { class MallPage extends GetView<MallController> {
const MallPage({super.key}); const MallPage({super.key});
Widget _buildView() {
return Stack(children: []);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<MallController>( return GetX<MallController>(
init: MallController(), init: MallController(),
id: 'mall',
builder: (_) { 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,
),
),
),
),
],
),
],
),
),
],
),
);
}
} }