Commit 7777aa65 authored by DatHV's avatar DatHV
Browse files

cập nhật cashback affiliate

parent 56e8b038
......@@ -34,4 +34,9 @@ class APIPaths {
static const String getGames = "/campaign/api/v3.0/games";
static const String verifyOrderProduct = "/order/api/v1.0/verify-product";
static const String getGameDetail = "/campaign/api/v3.0/games/%@/play";
static const String affiliateCategoryGetList = "/affiliateCategoryGetList/1.0.0";
static const String affiliateBrandGetList = "/affiliateBrandGetList/1.0.0";
static const String affiliateProductTopSale = "/affiliateProductTopSale/1.0.0";
static const String getCashbackOverview = "/cashback/overview";
static const String getCashbackStatus = "/cashback/status";
}
\ No newline at end of file
......@@ -13,6 +13,18 @@ extension NumExtension on num {
..minimumFractionDigits = 0;
return formatter.format(this);
}
String formatCompactNumber() {
if (this < 1000) {
return toString();
} else if (this < 1000000) {
return '${(this / 1000).toStringAsFixed(1)}K';
} else if (this < 1000000000) {
return '${(this / 1000000).toStringAsFixed(1)}M';
} else {
return '${(this / 1000000000).toStringAsFixed(1)}B';
}
}
}
enum CurrencyUnit {
......
......@@ -68,4 +68,16 @@ extension HexColorExtension on String {
}
return null;
}
}
extension ParseInt on String {
num? toNum() {
try {
final doubleValue = double.parse(this);
return doubleValue % 1 == 0 ? doubleValue.toInt() : doubleValue;
} catch (e) {
print('❌ String to Int parse failed for "$this": $e');
return 0;
}
}
}
\ No newline at end of file
......@@ -21,6 +21,10 @@ import '../screen/otp/model/create_otp_response_model.dart';
import '../screen/otp/model/otp_verify_response_model.dart';
import '../screen/pageDetail/model/campaign_detail_model.dart';
import '../screen/pageDetail/model/detail_page_rule_type.dart';
import '../screen/shopping/model/affiliate_brand_model.dart';
import '../screen/shopping/model/affiliate_category_model.dart';
import '../screen/shopping/model/affiliate_product_top_sale_model.dart';
import '../screen/shopping/model/cashback_overview_model.dart';
import '../screen/splash/splash_screen_viewmodel.dart';
import '../screen/voucher/models/like_product_reponse_model.dart';
import '../screen/voucher/models/product_store_model.dart';
......@@ -310,4 +314,37 @@ extension RestfullAPIClientAllApi on RestfulAPIClient {
return GameBundleItemModel.fromJson(data as Json);
});
}
Future<BaseResponseModel<List<AffiliateCategoryModel>>> affiliateCategoryGetList() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.affiliateCategoryGetList, Method.POST, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => AffiliateCategoryModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<List<AffiliateBrandModel>>> affiliateBrandGetList() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.affiliateBrandGetList, Method.POST, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => AffiliateBrandModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<List<AffiliateProductTopSaleModel>>> affiliateProductTopSale() async {
String? token = DataPreference.instance.token ?? "";
final body = {"access_token": token};
return requestNormal(APIPaths.affiliateProductTopSale, Method.POST, body, (data) {
final list = data as List<dynamic>;
return list.map((e) => AffiliateProductTopSaleModel.fromJson(e)).toList();
});
}
Future<BaseResponseModel<CashbackOverviewModel>> getCashBackOverview() async {
return requestNormal(APIPaths.getCashbackOverview, Method.GET, {}, (data) {
return CashbackOverviewModel.fromJson(data as Json);
});
}
}
......@@ -3,7 +3,7 @@ import '../../resouce/base_color.dart';
import '../game/game_tab_screen.dart';
import '../home/home_screen.dart';
import '../personal/personal_screen.dart';
import '../shopping/shopping_tab_screen.dart';
import '../shopping/affiliate_tab_screen.dart';
import '../voucher/voucher_tab_screen.dart';
class MainTabScreen extends StatefulWidget {
......@@ -20,7 +20,7 @@ class _MainTabScreenState extends State<MainTabScreen> {
HomeScreen(),
VoucherTabScreen(),
GameTabScreen(),
ShoppingTabScreen(),
AffiliateTabScreen(),
PersonalScreen(),
];
......
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
class AffiliateOverviewPopup extends StatelessWidget {
final List<String> descriptions;
const AffiliateOverviewPopup({super.key, required this.descriptions});
@override
Widget build(BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(4), // ✅ padding tổng thể
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Spacer(),
const Expanded(
flex: 6,
child: Text(
'Hoàn điểm là gì?',
textAlign: TextAlign.center, // ✅ căn giữa
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
),
),
Expanded(
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
)
],
),
const SizedBox(height: 12),
// Descriptions
...descriptions.map(
(html) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: HtmlWidget(html),
),
),
),
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/shopping/sub_widget/build_affiliate_brand.dart';
import 'package:mypoint_flutter_app/screen/shopping/sub_widget/build_affiliate_category.dart';
import 'package:mypoint_flutter_app/screen/shopping/sub_widget/build_affiliate_product_topsale.dart';
import '../../base/base_screen.dart';
import '../../base/basic_state.dart';
import '../../resouce/base_color.dart';
import '../../widgets/bottom_sheet_helper.dart';
import '../../widgets/custom_navigation_bar.dart';
import 'affiliate_overview.dart';
import 'affiliate_tab_viewmodel.dart';
class AffiliateTabScreen extends BaseScreen {
const AffiliateTabScreen({super.key});
@override
State<AffiliateTabScreen> createState() => _AffiliateTabScreenState();
}
class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicState {
final AffiliateTabViewModel viewModel = Get.put(AffiliateTabViewModel());
@override
Widget createBody() {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: CustomNavigationBar(
title: "Mua sắm",
showBackButton: false,
rightButtons: [
IconButton(
icon: const Icon(Icons.info, color: Colors.white),
onPressed: () {
final descriptions = viewModel.overview.value?.descriptions ?? [];
if (descriptions.isNotEmpty) {
BottomSheetHelper.showBottomSheetPopup(
child: AffiliateOverviewPopup(descriptions: viewModel.overview.value?.descriptions ?? []),
);
}
},
),
],
),
body: Obx(() {
if (viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: viewModel.refreshData,
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Text("Điểm hoàn:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.normal)),
const SizedBox(width: 8),
Image.asset('assets/images/ic_point.png', width: 20, height: 20),
const SizedBox(width: 4),
const Text(
"0", style: TextStyle(fontSize: 20, color: BaseColor.primary400, fontWeight: FontWeight.bold)),
const Spacer(),
Icon(Icons.arrow_forward_ios, color: BaseColor.primary400, size: 16),
],
),
),
const SizedBox(height: 16),
SizedBox(
height: 130,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_buildStepCard("assets/images/banner_tutorial_refund_point_step1.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step2.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step3.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step4.png"),
],
),
),
AffiliateBrand(brands: viewModel.affiliateBrands,),
AffiliateCategory(categories: viewModel.affiliateCategories,),
AffiliateProductTopSale(products: viewModel.affiliateProducts,),
],
),
),
);
}),
);
}
Widget _buildStepCard(String imagePath) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.asset(imagePath, fit: BoxFit.cover, width: 250),
);
}
}
import 'package:get/get_rx/src/rx_types/rx_types.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.dart';
import '../../base/restful_api_viewmodel.dart';
import 'model/affiliate_brand_model.dart';
import 'model/affiliate_category_model.dart';
import 'model/affiliate_category_type.dart';
import 'model/affiliate_product_top_sale_model.dart';
import 'model/cashback_overview_model.dart';
class AffiliateTabViewModel extends RestfulApiViewModel {
final RxList<AffiliateBrandModel> affiliateBrands = <AffiliateBrandModel>[].obs;
final RxList<AffiliateCategoryModel> affiliateCategories = <AffiliateCategoryModel>[].obs;
final RxList<AffiliateProductTopSaleModel> affiliateProducts = <AffiliateProductTopSaleModel>[].obs;
final RxBool isLoading = false.obs;
var overview = Rxn<CashbackOverviewModel>();
@override
void onInit() {
super.onInit();
refreshData();
_getAffiliateOverview();
}
Future<void> refreshData() async {
isLoading.value = true;
await Future.wait([
_getAffiliateBrandGetList(),
_getAffiliateCategoryGetList(),
_getAffiliateProductTopSale(),
]);
isLoading.value = false;
}
Future<void> _getAffiliateBrandGetList() async {
try {
final result = await client.affiliateBrandGetList();
affiliateBrands.value = (result.data ?? []).take(6).toList();
} catch (error) {
print("Error fetching affiliate brands: $error");
}
}
Future<void> _getAffiliateCategoryGetList() async {
try {
final result = await client.affiliateCategoryGetList();
final category = AffiliateCategoryModel(
code: AffiliateCategoryType.other,
name: "Khác",
);
final data = (result.data ?? []).take(7).toList();
data.add(category);
affiliateCategories.value = data;
} catch (error) {
print("Error fetching affiliate brands: $error");
}
}
Future<void> _getAffiliateProductTopSale() async {
try {
final result = await client.affiliateProductTopSale();
affiliateProducts.value = result.data ?? [];
} catch (error) {
print("Error fetching affiliate brands: $error");
}
}
Future<void> _getAffiliateOverview() async {
try {
final result = await client.getCashBackOverview();
overview.value = result.data;
} catch (error) {
print("Error fetching affiliate brands: $error");
}
}
}
\ No newline at end of file
import 'package:json_annotation/json_annotation.dart';
part 'product_brand_model.g.dart';
part 'affiliate_brand_model.g.dart';
@JsonSerializable()
class ProductBrandModel {
class AffiliateBrandModel {
@JsonKey(name: 'brand_id')
final String brandId;
@JsonKey(name: 'brand_name')
......@@ -15,7 +15,7 @@ class ProductBrandModel {
final String? categoryName;
final String? direction;
ProductBrandModel({
AffiliateBrandModel({
required this.brandId,
this.brandName,
this.commision,
......@@ -25,6 +25,6 @@ class ProductBrandModel {
this.direction,
});
factory ProductBrandModel.fromJson(Map<String, dynamic> json) => _$ProductBrandModelFromJson(json);
Map<String, dynamic> toJson() => _$ProductBrandModelToJson(this);
factory AffiliateBrandModel.fromJson(Map<String, dynamic> json) => _$AffiliateBrandModelFromJson(json);
Map<String, dynamic> toJson() => _$AffiliateBrandModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'product_brand_model.dart';
part of 'affiliate_brand_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ProductBrandModel _$ProductBrandModelFromJson(Map<String, dynamic> json) =>
ProductBrandModel(
AffiliateBrandModel _$AffiliateBrandModelFromJson(Map<String, dynamic> json) =>
AffiliateBrandModel(
brandId: json['brand_id'] as String,
brandName: json['brand_name'] as String?,
commision: json['commision'] as String?,
......@@ -17,13 +17,14 @@ ProductBrandModel _$ProductBrandModelFromJson(Map<String, dynamic> json) =>
direction: json['direction'] as String?,
);
Map<String, dynamic> _$ProductBrandModelToJson(ProductBrandModel instance) =>
<String, dynamic>{
'brand_id': instance.brandId,
'brand_name': instance.brandName,
'commision': instance.commision,
'logo': instance.logo,
'brand_url': instance.brandUrl,
'category_name': instance.categoryName,
'direction': instance.direction,
};
Map<String, dynamic> _$AffiliateBrandModelToJson(
AffiliateBrandModel instance,
) => <String, dynamic>{
'brand_id': instance.brandId,
'brand_name': instance.brandName,
'commision': instance.commision,
'logo': instance.logo,
'brand_url': instance.brandUrl,
'category_name': instance.categoryName,
'direction': instance.direction,
};
import 'package:json_annotation/json_annotation.dart';
import 'affiliate_category_type.dart';
part 'affiliate_category_model.g.dart';
@JsonSerializable()
class AffiliateCategoryModel {
@JsonKey(name: "category_code", fromJson: _codeFromJson, toJson: _codeToJson)
final AffiliateCategoryType code;
@JsonKey(name: "category_name")
final String name;
AffiliateCategoryModel({
required this.code,
required this.name,
});
static AffiliateCategoryType _codeFromJson(String code) =>
affiliateCategoryTypeValues[code] ?? AffiliateCategoryType.other;
static String _codeToJson(AffiliateCategoryType type) =>
affiliateCategoryTypeValues.entries
.firstWhere((e) => e.value == type,
orElse: () => const MapEntry("other", AffiliateCategoryType.other))
.key;
String get icon => "ic_cashback_${_codeToJson(code)}";
bool get isOther => code == AffiliateCategoryType.other;
static AffiliateCategoryModel get other =>
AffiliateCategoryModel(code: AffiliateCategoryType.other, name: "Khác");
factory AffiliateCategoryModel.fromJson(Map<String, dynamic> json) =>
_$AffiliateCategoryModelFromJson(json);
Map<String, dynamic> toJson() => _$AffiliateCategoryModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'affiliate_category_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AffiliateCategoryModel _$AffiliateCategoryModelFromJson(
Map<String, dynamic> json,
) => AffiliateCategoryModel(
code: AffiliateCategoryModel._codeFromJson(json['category_code'] as String),
name: json['category_name'] as String,
);
Map<String, dynamic> _$AffiliateCategoryModelToJson(
AffiliateCategoryModel instance,
) => <String, dynamic>{
'category_code': AffiliateCategoryModel._codeToJson(instance.code),
'category_name': instance.name,
};
enum AffiliateCategoryType {
laptop,
giayNu,
meVaBe,
doAn,
sucKhoe,
thoiTrangNu,
giaoDuc,
thoiTrangNam,
dienTu,
giaDung,
nhaCuaCuocSong,
phuKienDienThoai,
dongHo,
giayNam,
camera,
sacDep,
tuiViNu,
theThao,
duLich,
dichVu,
other,
}
const affiliateCategoryTypeValues = {
"may-tinh-and-laptop": AffiliateCategoryType.laptop,
"giay-dep-nu": AffiliateCategoryType.giayNu,
"me-and-be": AffiliateCategoryType.meVaBe,
"do-an": AffiliateCategoryType.doAn,
"suc-khoe": AffiliateCategoryType.sucKhoe,
"thoi-trang-nu": AffiliateCategoryType.thoiTrangNu,
"giao-duc": AffiliateCategoryType.giaoDuc,
"thoi-trang-nam": AffiliateCategoryType.thoiTrangNam,
"thiet-bi-dien-tu": AffiliateCategoryType.dienTu,
"thiet-bi-dien-gia-dung": AffiliateCategoryType.giaDung,
"nha-cua-and-doi-song": AffiliateCategoryType.nhaCuaCuocSong,
"dien-thoai-and-phu-kien": AffiliateCategoryType.phuKienDienThoai,
"dong-ho": AffiliateCategoryType.dongHo,
"giay-dep-nam": AffiliateCategoryType.giayNam,
"may-anh-and-may-quay-phim": AffiliateCategoryType.camera,
"sac-dep": AffiliateCategoryType.sacDep,
"tui-vi-nu": AffiliateCategoryType.tuiViNu,
"the-thao": AffiliateCategoryType.theThao,
"du-lich": AffiliateCategoryType.duLich,
"dich-vu": AffiliateCategoryType.dichVu,
"khac": AffiliateCategoryType.other,
};
import 'package:json_annotation/json_annotation.dart';
part 'brand_model.g.dart';
part 'affiliate_product_top_sale_model.g.dart';
@JsonSerializable()
class BrandModel {
class AffiliateProductTopSaleModel {
@JsonKey(name: 'product_name')
final String? productName;
@JsonKey(name: 'product_price')
......@@ -17,7 +17,7 @@ class BrandModel {
final String? quantitySold;
final String? direction;
BrandModel({
AffiliateProductTopSaleModel({
this.productName,
this.productPrice,
this.productLink,
......@@ -28,6 +28,6 @@ class BrandModel {
this.direction,
});
factory BrandModel.fromJson(Map<String, dynamic> json) => _$BrandModelFromJson(json);
Map<String, dynamic> toJson() => _$BrandModelToJson(this);
factory AffiliateProductTopSaleModel.fromJson(Map<String, dynamic> json) => _$AffiliateProductTopSaleModelFromJson(json);
Map<String, dynamic> toJson() => _$AffiliateProductTopSaleModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'brand_model.dart';
part of 'affiliate_product_top_sale_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
BrandModel _$BrandModelFromJson(Map<String, dynamic> json) => BrandModel(
AffiliateProductTopSaleModel _$AffiliateProductTopSaleModelFromJson(
Map<String, dynamic> json,
) => AffiliateProductTopSaleModel(
productName: json['product_name'] as String?,
productPrice: json['product_price'] as String?,
productLink: json['product_link'] as String?,
......@@ -17,14 +19,15 @@ BrandModel _$BrandModelFromJson(Map<String, dynamic> json) => BrandModel(
direction: json['direction'] as String?,
);
Map<String, dynamic> _$BrandModelToJson(BrandModel instance) =>
<String, dynamic>{
'product_name': instance.productName,
'product_price': instance.productPrice,
'product_link': instance.productLink,
'commision': instance.commision,
'logo': instance.logo,
'thumnail_link': instance.thumnailLink,
'quantity_sold': instance.quantitySold,
'direction': instance.direction,
};
Map<String, dynamic> _$AffiliateProductTopSaleModelToJson(
AffiliateProductTopSaleModel instance,
) => <String, dynamic>{
'product_name': instance.productName,
'product_price': instance.productPrice,
'product_link': instance.productLink,
'commision': instance.commision,
'logo': instance.logo,
'thumnail_link': instance.thumnailLink,
'quantity_sold': instance.quantitySold,
'direction': instance.direction,
};
import 'package:json_annotation/json_annotation.dart';
part 'cashback_overview_model.g.dart';
@JsonSerializable()
class CashbackOverviewModel {
final List<String>? descriptions;
final String? background;
final String? title;
final String? subtitle;
CashbackOverviewModel({
this.descriptions,
this.background,
this.title,
this.subtitle,
});
factory CashbackOverviewModel.fromJson(Map<String, dynamic> json) => _$CashbackOverviewModelFromJson(json);
Map<String, dynamic> toJson() => _$CashbackOverviewModelToJson(this);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cashback_overview_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CashbackOverviewModel _$CashbackOverviewModelFromJson(
Map<String, dynamic> json,
) => CashbackOverviewModel(
descriptions:
(json['descriptions'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
background: json['background'] as String?,
title: json['title'] as String?,
subtitle: json['subtitle'] as String?,
);
Map<String, dynamic> _$CashbackOverviewModelToJson(
CashbackOverviewModel instance,
) => <String, dynamic>{
'descriptions': instance.descriptions,
'background': instance.background,
'title': instance.title,
'subtitle': instance.subtitle,
};
import 'package:flutter/material.dart';
import '../../resouce/base_color.dart';
import '../../widgets/custom_navigation_bar.dart';
class ShoppingTabScreen extends StatefulWidget {
const ShoppingTabScreen({super.key});
@override
State<ShoppingTabScreen> createState() => _ShoppingTabScreenState();
}
class _ShoppingTabScreenState extends State<ShoppingTabScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: CustomNavigationBar(
title: "Mua sắm",
showBackButton: false,
rightButtons: [
IconButton(
icon: const Icon(Icons.info, color: Colors.white),
onPressed: () {
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Text("Điểm hoàn:", style: TextStyle(fontSize: 18, fontWeight: FontWeight.normal)),
const SizedBox(width: 8),
Image.asset('assets/images/ic_point.png', width: 20, height: 20),
const SizedBox(width: 4),
const Text("0", style: TextStyle(fontSize: 20, color: BaseColor.primary400, fontWeight: FontWeight.bold)),
const Spacer(),
Icon(Icons.arrow_forward_ios, color: BaseColor.primary400, size: 16),
],
),
),
const SizedBox(height: 16),
SizedBox(
height: 130,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
_buildStepCard("assets/images/banner_tutorial_refund_point_step1.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step2.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step3.png"),
const SizedBox(width: 12),
_buildStepCard("assets/images/banner_tutorial_refund_point_step4.png"),
],
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text("Thương Hiệu Hoàn Điểm", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
Text("Xem tất cả", style: TextStyle(color: BaseColor.primary400, fontSize: 14, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 12),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 3 / 3.2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
children: [
_buildBrandItem("Lazada", "30%", "assets/images/ic_pipi_06.png"),
_buildBrandItem("Newshop", "5,3%", "assets/images/ic_pipi_06.png"),
_buildBrandItem("Tiki", "0,78%", "assets/images/ic_pipi_06.png"),
_buildBrandItem("MyTour MyTour MyTour", "3,1%", "assets/images/ic_pipi_06.png"),
_buildBrandItem("Pierre Cardin", "10,2%", "assets/images/ic_pipi_06.png"),
_buildBrandItem("Adidas", "2,9%", "assets/images/ic_pipi_06.png"),
],
),
Text("Lĩnh Vực Hoàn Điểm", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
childAspectRatio: 3 / 3.2,
// crossAxisSpacing: 8,
children: [
_fieldCashBackItem("Lazada", "30%", "assets/images/ic_pipi_06.png"),
_fieldCashBackItem("Newshop", "5,3%", "assets/images/ic_pipi_06.png"),
_fieldCashBackItem("Tiki", "0,78%", "assets/images/ic_pipi_06.png"),
_fieldCashBackItem("MyTour MyTour MyTour", "3,1%", "assets/images/ic_pipi_06.png"),
_fieldCashBackItem("Pierre Cardin", "10,2%", "assets/images/ic_pipi_06.png"),
_fieldCashBackItem("Adidas", "2,9%", "assets/images/ic_pipi_06.png"),
],
),
),
],
),
),
);
}
Widget _buildStepCard(String imagePath) {
return ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.asset(imagePath, fit: BoxFit.cover, width: 250),
);
}
Widget _fieldCashBackItem(String name, String cashback, String logoPath) {
return LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2;
return Container(
padding: const EdgeInsets.all(12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: imageWidth,
height: imageWidth, // ✅ 1:1 ratio
child: Image.asset(logoPath, fit: BoxFit.contain),
),
const SizedBox(height: 4),
Text(
textAlign: TextAlign.center,
name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16, color: BaseColor.second500),
),
],
),
);
},
);
}
Widget _buildBrandItem(String name, String cashback, String logoPath) {
return LayoutBuilder(
builder: (context, constraints) {
final double imageWidth = constraints.maxWidth / 2;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: imageWidth,
height: imageWidth, // ✅ 1:1 ratio
child: Image.asset(logoPath, fit: BoxFit.contain),
),
const SizedBox(height: 4),
Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16),
),
const SizedBox(height: 4),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: const TextStyle(fontSize: 12),
children: [
const TextSpan(
text: "Hoàn đến: ",
style: TextStyle(color: Colors.grey),
),
TextSpan(
text: cashback,
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
},
);
}
}
import '../../base/restful_api_viewmodel.dart';
class ShoppingTabViewModel extends RestfulApiViewModel {
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment