积分兑换首页
This commit is contained in:
BIN
ln_jq_app/assets/images/mall_bar@2x.png
Normal file
BIN
ln_jq_app/assets/images/mall_bar@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 896 B |
@@ -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 跳转
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user