初始化
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'col_his_buy_page.dart';
|
||||
import 'col_his_buy_sub_page.dart';
|
||||
|
||||
/// 收藏/购买/历史/喜欢共用:四种用途只差 tab 列表和能不能编辑
|
||||
class ColHisBuyLogic extends GetxController with GetSingleTickerProviderStateMixin {
|
||||
final PageType type;
|
||||
|
||||
ColHisBuyLogic(this.type);
|
||||
|
||||
//喜欢没有小说
|
||||
late final List<LoadDataType> tabs = [
|
||||
LoadDataType.video,
|
||||
LoadDataType.short,
|
||||
LoadDataType.drama,
|
||||
LoadDataType.post,
|
||||
LoadDataType.comics,
|
||||
LoadDataType.cartoon,
|
||||
LoadDataType.pictures,
|
||||
if (type != PageType.like) LoadDataType.novel,
|
||||
];
|
||||
|
||||
late final tabCtr = TabController(length: tabs.length, vsync: this);
|
||||
|
||||
//编辑态:列表项右上角挂删除按钮
|
||||
bool isEdit = false;
|
||||
|
||||
//已购没有删除接口,不给编辑入口
|
||||
bool get canEdit => type != PageType.buy;
|
||||
|
||||
void toggleEdit() {
|
||||
isEdit = !isEdit;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import 'col_his_buy_logic.dart';
|
||||
import 'col_his_buy_sub_page.dart';
|
||||
|
||||
/// 一个页面壳四种用途,每种钉一个独立 route。
|
||||
/// 这些名字没注册进 AppRoutes,只能走 [ColHisBuyPage.to],不能 Get.toNamed
|
||||
enum PageType {
|
||||
collect('我的收藏', '/MineCollectPage'),
|
||||
buy('我的购买', '/MineBuyPage'),
|
||||
history('历史记录', '/MineHistoryPage'),
|
||||
like('我的喜欢', '/MineLikePage');
|
||||
|
||||
final String title;
|
||||
final String route;
|
||||
|
||||
const PageType(this.title, this.route);
|
||||
}
|
||||
|
||||
/// 收藏 / 购买 / 历史 / 喜欢
|
||||
class ColHisBuyPage extends StatelessWidget {
|
||||
final PageType type;
|
||||
|
||||
const ColHisBuyPage(this.type, {super.key});
|
||||
|
||||
static Future<T?>? to<T>(PageType type) =>
|
||||
Get.to(() => ColHisBuyPage(type), routeName: type.route);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<ColHisBuyLogic>(
|
||||
init: ColHisBuyLogic(type),
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(type.title),
|
||||
actions: [
|
||||
//已购没有删除接口,不给编辑入口
|
||||
if (logic.canEdit)
|
||||
GestureDetector(
|
||||
onTap: logic.toggleEdit,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: Text(
|
||||
logic.isEdit ? '完成' : '编辑',
|
||||
style: textStyle(14, Color(0xff666666), FontWeight.w400),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
//分类 tab
|
||||
Container(
|
||||
color: Get.theme.appBarTheme.backgroundColor,
|
||||
child: TabBar(
|
||||
labelPadding: EdgeInsets.zero,
|
||||
padding: EdgeInsets.only(left: 8.w),
|
||||
tabAlignment: TabAlignment.start,
|
||||
indicator: CustomIndicator(
|
||||
width: 16,
|
||||
height: 3,
|
||||
isGradient: true,
|
||||
),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Color(0xffcccccc), fontSize: 14, height: 1),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
isScrollable: true,
|
||||
tabs: logic.tabs
|
||||
.map(
|
||||
(e) => Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12.w, vertical: 8.h),
|
||||
child: Text(e.title),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: logic.tabCtr,
|
||||
),
|
||||
),
|
||||
//各分类列表
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: logic.tabCtr,
|
||||
children: logic.tabs
|
||||
.map((e) => ColHisBuySubPage(e,
|
||||
pageType: type, isEdit: logic.isEdit)
|
||||
.keepAlive)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/cartoon_media_info.dart';
|
||||
import 'package:hgdj/hj_model/drama_media_info.dart';
|
||||
import 'package:hgdj/hj_model/list_base_model.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/acg_service.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/history_util.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
import 'package:hgdj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
|
||||
import '../../cartoon/acg_widget_item.dart';
|
||||
import '../../community/widget/community_post_widget.dart';
|
||||
import '../../community/widget/pic_simple_cell.dart';
|
||||
import '../../drama/drama_detail_page.dart';
|
||||
import '../../home/home_cell_style/tiktok_simple_cell.dart';
|
||||
import '../../home/home_cell_style/video_simple_cell.dart';
|
||||
import '../widgets/stage_cell_widget.dart';
|
||||
import 'col_his_buy_page.dart';
|
||||
import 'col_his_buy_sub_page.dart';
|
||||
|
||||
/// 用途 × 分类 决定用哪个 logic:短剧四种用途同一套 DramaInfo 列表,其余按用途分
|
||||
ColHisBuySubLogic instanceLogic(PageType pageType, LoadDataType loadType) {
|
||||
if (loadType == LoadDataType.drama) return DramaLogic(loadType, pageType);
|
||||
return switch (pageType) {
|
||||
PageType.collect => CollectLogic(loadType),
|
||||
PageType.buy =>
|
||||
loadType.isAcg ? BuyAcgLogic(loadType) : BuyVideoLogic(loadType),
|
||||
PageType.history => HistoryLogic(loadType),
|
||||
PageType.like => LikeLogic(loadType),
|
||||
};
|
||||
}
|
||||
|
||||
/// 收藏/购买/历史/喜欢的子列表基类:分页刷新和 item 渲染都在这,子类只管取数和删除
|
||||
abstract class ColHisBuySubLogic extends ListBaseLogic {
|
||||
final LoadDataType loadType;
|
||||
|
||||
ColHisBuySubLogic(this.loadType);
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData({bool isRefresh = true}) =>
|
||||
fetchData(isRefresh: isRefresh, fetch: fetch);
|
||||
|
||||
/// 返回 (本页数据, 是否还有下一页),页码由基类算好传进来
|
||||
Future<(List?, bool)> fetch(int page);
|
||||
|
||||
/// 本列表是否支持删除。false 时编辑态不挂删除按钮,免得点了没反应
|
||||
bool get canDelete => true;
|
||||
|
||||
/// 取消收藏 / 删除一条,不支持的列表不用覆写
|
||||
Future<void> deleteAt(int index) async {}
|
||||
|
||||
Widget itemAt(int index) {
|
||||
final model = dataList![index];
|
||||
return switch (loadType) {
|
||||
LoadDataType.video => VideoSimpleCell(videoModel: model),
|
||||
LoadDataType.game =>
|
||||
VideoSimpleCell(videoModel: model, isFromHY: true, textLines: 1),
|
||||
LoadDataType.short => TiktokSimpleCell(
|
||||
videoModel: model,
|
||||
textLines: 1,
|
||||
isShowBottom: true,
|
||||
isShowTime: true),
|
||||
LoadDataType.pictures => PicSimpleCell(videoModel: model, textLines: 1),
|
||||
LoadDataType.post => CommunityPostWidget(videoModel: model),
|
||||
LoadDataType.collections => SimpleCollectionsItem(model),
|
||||
LoadDataType.comics ||
|
||||
LoadDataType.cartoon ||
|
||||
LoadDataType.novel =>
|
||||
AcgItemWidget(info: model),
|
||||
//短剧卡片右下角挂集数状态,点进去续播
|
||||
LoadDataType.drama => VideoSimpleCell(
|
||||
videoModel: model,
|
||||
textLines: 1,
|
||||
coverRightText: (model as VideoModel).dramaInfo?.episodeNumberStatus,
|
||||
showLevelIcon: false,
|
||||
onTap: () => _openDrama(model),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 进短剧二级页,不指定集数(有续播位置就接着上次播)。
|
||||
/// 退回来重拉一次列表:历史按最后观看时间排,刚看的这部要跳到最前;
|
||||
/// 收藏/喜欢那几个列表也可能在二级页里被改过状态
|
||||
Future<void> _openDrama(VideoModel model) async {
|
||||
await Get.to(() => DramaDetailPage(drama: model.dramaInfo));
|
||||
if (isClosed) return; // 二级页开着时用户退了本页
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
/// 短剧的收藏/已购/喜欢/历史:接口不同但返回的都是 DramaInfo 列表,
|
||||
/// 统一转成 VideoModel 交给和热门短剧橱窗同一个卡片渲染
|
||||
class DramaLogic extends ColHisBuySubLogic {
|
||||
final PageType pageType;
|
||||
|
||||
DramaLogic(super.loadType, this.pageType);
|
||||
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
//历史记录只在本地:播放时写进 dramaResume 那张表,按最后观看时间倒序
|
||||
final list = pageType == PageType.history
|
||||
? (await dramaResume.history(page: page, pageSize: _pageSize))
|
||||
.map((e) => e.drama!)
|
||||
.toList()
|
||||
: (await _api(page))?.list ?? [];
|
||||
//本地和接口都不看 hasNext,满页就当还有
|
||||
return (
|
||||
list.map((e) => e.toVideoModel(null)).toList(),
|
||||
list.length >= _pageSize
|
||||
);
|
||||
}
|
||||
|
||||
Future<ListBaseModel<DramaMediaInfo>?> _api(int page) => switch (pageType) {
|
||||
PageType.buy => DramaService.fetchPurchased(page, size: _pageSize),
|
||||
PageType.like => DramaService.fetchLikes(page, size: _pageSize),
|
||||
_ => DramaService.fetchFavorites(page, size: _pageSize),
|
||||
};
|
||||
|
||||
//已购没有删除接口,编辑态不给删除按钮;历史是本地表,能删
|
||||
@override
|
||||
bool get canDelete => pageType != PageType.buy;
|
||||
|
||||
@override
|
||||
Future<void> deleteAt(int index) async {
|
||||
if (!canDelete) return;
|
||||
final id = (dataList![index] as VideoModel).dramaInfo?.id ?? '';
|
||||
switch (pageType) {
|
||||
case PageType.history:
|
||||
await dramaResume.erase(id);
|
||||
case PageType.collect:
|
||||
await ACGService.deleteBookshelf(id);
|
||||
default:
|
||||
await CommonService.cancelLike(id, 'drama');
|
||||
}
|
||||
dataList!.removeAt(index);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/// 浏览历史,全部读本地库
|
||||
class HistoryLogic extends ColHisBuySubLogic {
|
||||
HistoryLogic(super.loadType);
|
||||
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
final List list = loadType.isAcg
|
||||
? await HistoryUtil.fetch<CartoonMediaInfo>(loadType.historyType,
|
||||
page: page, pageSize: _pageSize)
|
||||
: await HistoryUtil.fetch<VideoModel>(loadType.historyType,
|
||||
page: page, pageSize: _pageSize);
|
||||
//满页说明还有更多,不满即到底
|
||||
return (list, list.length >= _pageSize);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAt(int index) async {
|
||||
final model = dataList![index];
|
||||
if (model is! VideoModel && model is! CartoonMediaInfo) {
|
||||
debugLog('删除类型未处理');
|
||||
return;
|
||||
}
|
||||
await HistoryUtil.delete(model, loadType.historyType);
|
||||
dataList!.remove(model);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/// 已购的漫画/动漫/小说
|
||||
class BuyAcgLogic extends ColHisBuySubLogic {
|
||||
BuyAcgLogic(super.loadType);
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
final res = await ACGService.fetchAcgBuyData<CartoonMediaInfo>(page, 12,
|
||||
mediaType: loadType.apiType);
|
||||
return (res?.list, res?.hasNext ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 已购的影视/短视频/帖子/图集
|
||||
class BuyVideoLogic extends ColHisBuySubLogic {
|
||||
BuyVideoLogic(super.loadType);
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
final res = await MineService.fetchBuyVideo(
|
||||
newsType: loadType.apiType, page: page, size: 12);
|
||||
return (res?.list, res?.hasNext ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 我的喜欢,取数和取消都是同一套 like 接口
|
||||
class LikeLogic extends ColHisBuySubLogic {
|
||||
LikeLogic(super.loadType);
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
final res = await MineService.fetchLikes(
|
||||
likeType: loadType.apiType, page: page, size: 12);
|
||||
return (res?.list, res?.hasNext ?? false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAt(int index) async {
|
||||
await CommonService.cancelLike(dataList![index].id, loadType.apiType);
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
/// 我的收藏,漫画/动漫/小说在书架接口,其余在收藏接口
|
||||
class CollectLogic extends ColHisBuySubLogic {
|
||||
CollectLogic(super.loadType);
|
||||
|
||||
@override
|
||||
Future<(List?, bool)> fetch(int page) async {
|
||||
final ListBaseModel? res = loadType.isAcg
|
||||
? await ACGService.fetchAcgBooklib<CartoonMediaInfo>(loadType.apiType,
|
||||
page: page, size: 12)
|
||||
: await MineService.fetchCollectList<VideoModel>(loadType.apiType,
|
||||
page: page, size: 12);
|
||||
return (res?.list, res?.hasNext ?? false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAt(int index) async {
|
||||
final model = dataList![index];
|
||||
final bool isDeleted;
|
||||
if (model is VideoModel) {
|
||||
isDeleted =
|
||||
await MineService.postCollect(model.id, loadType.apiType, false);
|
||||
} else if (model is CartoonMediaInfo) {
|
||||
isDeleted = await ACGService.deleteBookshelf(model.id ?? '');
|
||||
} else {
|
||||
debugLog('删除类型未处理');
|
||||
return;
|
||||
}
|
||||
if (!isDeleted) return;
|
||||
dataList!.remove(model);
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/mine/collec_history_buy/col_his_buy_page.dart';
|
||||
import 'package:hgdj/hj_page/mine/collec_history_buy/col_his_buy_sub_logic.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
enum LoadDataType {
|
||||
/// 长视频
|
||||
video(
|
||||
title: '影视',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'SP',
|
||||
crossAxisCount: 2,
|
||||
spaceV: 12,
|
||||
spaceH: 8,
|
||||
padding: EdgeInsets.only(left: 20, right: 20, top: 14),
|
||||
ratio: 174 / 160,
|
||||
historyType: MediaStyle.Video,
|
||||
),
|
||||
|
||||
/// 短视频
|
||||
short(
|
||||
title: '抖音',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'SHORT',
|
||||
crossAxisCount: 3,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
ratio: 115 / 192,
|
||||
historyType: MediaStyle.ShortVideo,
|
||||
),
|
||||
|
||||
/// 短剧 media_bookshelf/list, media/my_buy, mine/like;历史走本地 DramaResumeStore
|
||||
drama(
|
||||
title: '短剧',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'drama',
|
||||
crossAxisCount: 2,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 14),
|
||||
ratio: 168 / 266,
|
||||
historyType: MediaStyle.Drama,
|
||||
),
|
||||
|
||||
/// 帖子
|
||||
post(
|
||||
title: '帖子',
|
||||
style: PageStyle.list,
|
||||
apiType: 'COVER',
|
||||
crossAxisCount: 2,
|
||||
spaceV: 10,
|
||||
spaceH: 14,
|
||||
padding: EdgeInsets.only(left: 0, right: 0, top: 14),
|
||||
ratio: 191 / 174,
|
||||
historyType: MediaStyle.Community,
|
||||
),
|
||||
|
||||
/// 漫画 media_bookshelf/list, media/my_buy
|
||||
comics(
|
||||
title: '漫画',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'image',
|
||||
crossAxisCount: 3,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 14),
|
||||
ratio: 115 / 192,
|
||||
historyType: MediaStyle.Comics,
|
||||
),
|
||||
|
||||
/// 动漫 media_bookshelf/list, media/my_buy
|
||||
cartoon(
|
||||
title: '动漫',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'video',
|
||||
crossAxisCount: 3,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 14),
|
||||
ratio: 115 / 192,
|
||||
historyType: MediaStyle.Cartoon,
|
||||
),
|
||||
|
||||
/// 图集
|
||||
pictures(
|
||||
title: '图集',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'PIC',
|
||||
crossAxisCount: 3,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
ratio: 115 / 192,
|
||||
historyType: MediaStyle.Pic,
|
||||
),
|
||||
|
||||
/// 小说 media_bookshelf/list, media/my_buy
|
||||
novel(
|
||||
title: '小说',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'text',
|
||||
crossAxisCount: 3,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 14),
|
||||
ratio: 115 / 192,
|
||||
historyType: MediaStyle.Novel,
|
||||
),
|
||||
|
||||
/// 种子/黄油帖子(tab 暂时关掉,配置留着)
|
||||
game(
|
||||
title: '种子',
|
||||
style: PageStyle.grid,
|
||||
apiType: 'game',
|
||||
crossAxisCount: 2,
|
||||
spaceV: 12,
|
||||
spaceH: 8,
|
||||
padding: EdgeInsets.only(left: 20, right: 20, top: 14),
|
||||
ratio: 174 / 160,
|
||||
historyType: MediaStyle.Game,
|
||||
),
|
||||
|
||||
/// 合集(tab 暂时关掉,配置留着)
|
||||
collections(
|
||||
title: '合集',
|
||||
style: PageStyle.list,
|
||||
apiType: 'collection',
|
||||
crossAxisCount: 2,
|
||||
spaceV: 12,
|
||||
spaceH: 6,
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 14),
|
||||
ratio: 115 / 175,
|
||||
historyType: MediaStyle.Cartoon,
|
||||
);
|
||||
|
||||
/// tab 标题
|
||||
final String title;
|
||||
|
||||
/// 页面风格 列表、网格
|
||||
final PageStyle style;
|
||||
|
||||
/// 接口的类型参数(收藏 type / 已购 mediaType / 喜欢 likeType 是同一个值)
|
||||
final String apiType;
|
||||
|
||||
/// 针对网格 纵向多少个
|
||||
final int crossAxisCount;
|
||||
|
||||
/// 网格 垂直间隔
|
||||
final double spaceV;
|
||||
|
||||
/// 网格 横向间隔
|
||||
final double spaceH;
|
||||
|
||||
/// 页面间距
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// 网格宽高比
|
||||
final double ratio;
|
||||
|
||||
/// 历史记录类型
|
||||
final MediaStyle historyType;
|
||||
|
||||
const LoadDataType({
|
||||
required this.title,
|
||||
required this.style,
|
||||
required this.apiType,
|
||||
required this.crossAxisCount,
|
||||
required this.spaceV,
|
||||
required this.spaceH,
|
||||
required this.padding,
|
||||
required this.ratio,
|
||||
required this.historyType,
|
||||
});
|
||||
|
||||
/// 漫画/动漫/小说走 ACG 那套接口,返回 CartoonMediaInfo;其余走 VideoModel
|
||||
bool get isAcg => this == comics || this == cartoon || this == novel;
|
||||
}
|
||||
|
||||
enum PageStyle {
|
||||
list,
|
||||
grid;
|
||||
}
|
||||
|
||||
class ColHisBuySubPage extends StatelessWidget {
|
||||
final LoadDataType loadType;
|
||||
final PageType pageType;
|
||||
final bool isEdit;
|
||||
|
||||
const ColHisBuySubPage(
|
||||
this.loadType, {
|
||||
super.key,
|
||||
required this.pageType,
|
||||
this.isEdit = false,
|
||||
});
|
||||
|
||||
//一个页面里 8 个 tab 的 logic 同时活着,按用途+分类隔离;
|
||||
//用 .name 而非 toString(),它是编译期常量,release 混淆不会改
|
||||
String get _tag => '${pageType.name}_${loadType.name}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<ColHisBuySubLogic>(
|
||||
tag: _tag,
|
||||
init: instanceLogic(pageType, loadType),
|
||||
builder: (logic) {
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
onRefresh: (_) => logic.loadData(),
|
||||
onLoading: (_) => logic.loadData(isRefresh: false),
|
||||
child: () {
|
||||
if (logic.isLoading) return LoadingCenterWidget();
|
||||
if (logic.isEmptyData) return CErrorWidget();
|
||||
return _list(logic);
|
||||
}(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _list(ColHisBuySubLogic logic) {
|
||||
final count = logic.dataList!.length;
|
||||
if (loadType.style == PageStyle.list) {
|
||||
return ListView.separated(
|
||||
separatorBuilder: (_, __) => 14.sizeBoxH,
|
||||
itemCount: count,
|
||||
padding: loadType.padding,
|
||||
itemBuilder: (_, index) => _item(logic, index, StackFit.loose),
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
padding: loadType.padding,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: loadType.crossAxisCount,
|
||||
mainAxisSpacing: loadType.spaceV,
|
||||
crossAxisSpacing: loadType.spaceH,
|
||||
childAspectRatio: loadType.ratio,
|
||||
),
|
||||
itemCount: count,
|
||||
itemBuilder: (_, index) => _item(logic, index, StackFit.expand),
|
||||
);
|
||||
}
|
||||
|
||||
//列表项 + 编辑态右上角删除按钮
|
||||
Widget _item(ColHisBuySubLogic logic, int index, StackFit fit) {
|
||||
return Stack(
|
||||
fit: fit,
|
||||
children: [
|
||||
logic.itemAt(index),
|
||||
if (isEdit && logic.canDelete)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: GestureDetector(
|
||||
onTap: () => logic.deleteAt(index),
|
||||
child: Image.asset('mine_col_delete.png'.mineImgPath, width: 20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/light_model.dart';
|
||||
import 'package:hgdj/hj_utils/store_keys.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_alert.dart';
|
||||
import 'package:hgdj/track_event_manager/device_service.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../config/config.dart';
|
||||
import '../../hj_model/user/user_info_model.dart';
|
||||
import '../../routers/jump_router.dart';
|
||||
import '../main_page/provider/msg_provider.dart';
|
||||
import '../web_page/h5_page.dart';
|
||||
import 'collec_history_buy/col_his_buy_page.dart';
|
||||
import 'identity/mine_identity_page.dart';
|
||||
import 'message/message_center_page.dart';
|
||||
import 'mine_drive/mine_drive_page.dart';
|
||||
import 'mine_following/mine_following_view.dart';
|
||||
import 'mine_post/mine_publish_main_page.dart';
|
||||
import 'mine_setting/mine_code_page.dart';
|
||||
import 'mine_setting/mine_password_logic.dart';
|
||||
import 'mine_setting/mine_password_page.dart';
|
||||
import 'mine_setting/mine_setting_page.dart';
|
||||
import 'mine_share/mine_share_page.dart';
|
||||
import 'more_question/mine_feedback_page.dart';
|
||||
import 'welfare/welfare_home_page.dart';
|
||||
|
||||
//图标
|
||||
class IconBean {
|
||||
String image;
|
||||
String name;
|
||||
|
||||
IconBean(this.image, this.name);
|
||||
}
|
||||
|
||||
class MineMainLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
RefreshController? refreshCtr;
|
||||
|
||||
UserInfoModel? get userInfo => globalStore.meInfo;
|
||||
|
||||
List<String> get menuVerNameArr => [
|
||||
"意见反馈",
|
||||
"我的购买",
|
||||
"我的喜欢",
|
||||
"分享邀请",
|
||||
"应用推荐",
|
||||
"账号凭证",
|
||||
"锁屏密码",
|
||||
"我的消息",
|
||||
];
|
||||
|
||||
List<IconBean> iconList = [
|
||||
IconBean('ic_launcher.webp'.commonImgPath, Config.appName),
|
||||
IconBean('icon_1.webp'.appImgPath, "爱音乐"),
|
||||
IconBean('icon_2.webp'.appImgPath, "爱阅读"),
|
||||
IconBean('icon_3.webp'.appImgPath, "支付宝"),
|
||||
IconBean('icon_4.webp'.appImgPath, "哔哩哔哩"),
|
||||
IconBean('icon_5.webp'.appImgPath, "饼干大作战"),
|
||||
IconBean('icon_6.webp'.appImgPath, "美食借鉴"),
|
||||
IconBean('icon_7.webp'.appImgPath, "我的世界"),
|
||||
IconBean('icon_8.webp'.appImgPath, "神选之战"),
|
||||
IconBean('icon_9.webp'.appImgPath, "随心无线"),
|
||||
IconBean('icon_10.webp'.appImgPath, "特轩小说"),
|
||||
IconBean('icon_11.webp'.appImgPath, "天天麻将"),
|
||||
IconBean('icon_12.webp'.appImgPath, "途游"),
|
||||
IconBean('icon_13.webp'.appImgPath, "微信"),
|
||||
IconBean('icon_14.webp'.appImgPath, "问道"),
|
||||
IconBean('icon_15.webp'.appImgPath, "英语图书角"),
|
||||
];
|
||||
|
||||
@override
|
||||
void onReady() async {
|
||||
super.onReady();
|
||||
bool savedQR = await lightKV.getBool(StoreKeys.HAVE_SAVE_QR_CODE) ?? false;
|
||||
if (!savedQR) {
|
||||
lightKV.setBool(StoreKeys.HAVE_SAVE_QR_CODE, true);
|
||||
await _showSaveQrDialog();
|
||||
}
|
||||
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
Future<void> getUserInfo() async {
|
||||
await globalStore.updateUserInfo();
|
||||
}
|
||||
|
||||
Future<void> getWallet() async {
|
||||
await globalStore.refreshWallet();
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
await Future.wait([
|
||||
getUserInfo(),
|
||||
MineMsgProvider().getMessageTip(),
|
||||
PreSaleProvider().refreshAll(), //刷新预售
|
||||
MineMsgProvider().refreshPayPopup(), //刷新支付分层配置(状态变则清会员卡缓存 + 重启倒计时)
|
||||
getWallet(),
|
||||
]).whenComplete(() {
|
||||
refreshCtr?.refreshCompleted();
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
/// 首次进入二维码页面提示保存二维码
|
||||
Future<dynamic> _showSaveQrDialog() async {
|
||||
final value = await Get.dialog(MineAccountIdentityPage());
|
||||
if (value != null && value is UserInfoModel) {
|
||||
update();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void menuTapEvent(int index) async {
|
||||
if (index == 0) {
|
||||
//我的帖子
|
||||
Get.to(MinePublishPostPage(), preventDuplicates: false);
|
||||
} else if (index == 1) {
|
||||
//我的喜欢
|
||||
ColHisBuyPage.to(PageType.like);
|
||||
} else if (index == 2) {
|
||||
//我的关注
|
||||
Get.to(() => MineFollowingPage());
|
||||
} else if (index == 3) {
|
||||
//消息中心
|
||||
await Get.to(() => MessageCenterPage(),
|
||||
opaque: true, preventDuplicates: false);
|
||||
MineMsgProvider().getMessageTip();
|
||||
}
|
||||
}
|
||||
|
||||
void menuVerTapEvent(int index) async {
|
||||
if (index == 0) {
|
||||
//创作中心
|
||||
Get.to(MinePublishPostPage(), preventDuplicates: false);
|
||||
} else if (index == 1) {
|
||||
//意见反馈
|
||||
Get.to(() => const MineFeedbackPage());
|
||||
} else if (index == 2) {
|
||||
//我的购买
|
||||
ColHisBuyPage.to(PageType.buy);
|
||||
} else if (index == 3) {
|
||||
//我的喜欢
|
||||
ColHisBuyPage.to(PageType.like);
|
||||
} else if (index == 4) {
|
||||
//分享邀请
|
||||
Get.to(() => MineSharePage());
|
||||
} else if (index == 5) {
|
||||
//领取兑换
|
||||
Get.to(() => MineExchangeCodePage(type: 1));
|
||||
} else if (index == 6) {
|
||||
//加群开车
|
||||
Get.to(() => MineDrivePage());
|
||||
} else if (index == 7) {
|
||||
//应用推荐
|
||||
Get.to(() => const WelfareHomePage(index: 1), preventDuplicates: false);
|
||||
} else if (index == 8) {
|
||||
//账号凭证
|
||||
await Get.dialog(MineAccountIdentityPage());
|
||||
} else if (index == 9) {
|
||||
//锁屏密码
|
||||
if (globalStore.password.isNotEmpty) {
|
||||
final close = await CommonAlert.show(
|
||||
title: '关闭锁定码',
|
||||
content: '您确定关闭锁定密码?\n\n开启需重新输入密码',
|
||||
cancelText: '稍后再说',
|
||||
confirmText: '确定关闭',
|
||||
);
|
||||
if (close) {
|
||||
Get.toNamed(MinePasswordPage.routeName,
|
||||
arguments: MinePasswordType.close);
|
||||
}
|
||||
} else {
|
||||
Get.toNamed(
|
||||
MinePasswordPage.routeName,
|
||||
arguments: MinePasswordType.setting,
|
||||
);
|
||||
}
|
||||
} else if (index == 10) {
|
||||
menuTapEvent(3);
|
||||
} else if (index == 11) {
|
||||
pushToCustomService();
|
||||
}
|
||||
}
|
||||
|
||||
toSetting() {
|
||||
Get.to(() => const MineSettingPage());
|
||||
}
|
||||
|
||||
toMineShare() {
|
||||
Get.to(() => MineSharePage());
|
||||
}
|
||||
|
||||
toExchangeCenter() {
|
||||
Get.to(() => const WelfareHomePage(index: 1));
|
||||
}
|
||||
|
||||
toShop() async {
|
||||
LoadingHelper.showLoading();
|
||||
var result = await VidService.getShopAddress();
|
||||
LoadingHelper.dismissLoading();
|
||||
if (result is String && result.isNotEmpty != true) {
|
||||
showToast("获取商店地址失败~");
|
||||
return;
|
||||
}
|
||||
Get.to(H5Page(url: result, showTitle: false, showClose: true),
|
||||
popGesture: true);
|
||||
}
|
||||
|
||||
changeIcon(int index) async {
|
||||
if (!await CommonAlert.show(content: '您确定要更换图标吗?')) return;
|
||||
const platform = MethodChannel(DeviceInfoService.requestChannel);
|
||||
platform.invokeMethod('changeIcon', {"iconIndex": index});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/address.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/hj_page/main_page/provider/msg_provider.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../hj_utils/widget_util.dart';
|
||||
import '../../tools_base/banner/ads_grid_view_widget.dart';
|
||||
import 'home_mine_logic.dart';
|
||||
import 'widgets/mine_info_widget.dart';
|
||||
import 'widgets/vip_entry_card.dart';
|
||||
|
||||
//我的页面
|
||||
class HomeMinePage extends StatelessWidget {
|
||||
const HomeMinePage({super.key});
|
||||
|
||||
/// 常用功能里唯一需要展示未读红点的入口
|
||||
static const _messageMenuTitle = '我的消息';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineMainLogic>(
|
||||
init: MineMainLogic(),
|
||||
builder: (logic) {
|
||||
return Consumer<GlobalStore>(builder: (_, store, ___) {
|
||||
return Scaffold(
|
||||
body: SizedBox.expand(
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
enablePullUp: false,
|
||||
onRefresh: (ctr) => logic.onRefresh(),
|
||||
child: SingleChildScrollView(
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildBg(),
|
||||
),
|
||||
SafeArea(child: _buildContent(logic, store)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//顶部背景:优先后台下发,兜底默认图
|
||||
Widget _buildBg() {
|
||||
final bg = Config.mineBg;
|
||||
if (bg != null && bg.isNotEmpty) {
|
||||
return NetworkImageLoader(imageUrl: bg, borderRadius: 0);
|
||||
}
|
||||
return Image.asset(
|
||||
'mine_bg.webp'.mineImgPath,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(MineMainLogic logic, GlobalStore store) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildTop(logic),
|
||||
Column(
|
||||
children: [
|
||||
48.sizeBoxH,
|
||||
MineInfoWidget(logic: logic),
|
||||
21.sizeBoxH,
|
||||
_buildCountView(), //次数
|
||||
21.sizeBoxH,
|
||||
VipEntryCard(), //会员入口
|
||||
12.sizeBoxH,
|
||||
MineTaskMenuWidget(), //金币充值等
|
||||
12.sizeBoxH,
|
||||
MineFunchtionGridView(), //收藏/历史/下载/关注
|
||||
12.sizeBoxH,
|
||||
_buildShop(logic), //商店入口
|
||||
AdsGridViewWidget(
|
||||
8,
|
||||
accordingAdsType: true,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 5),
|
||||
child: Text("常用功能",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
)),
|
||||
),
|
||||
GridView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: logic.menuVerNameArr.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 0,
|
||||
mainAxisSpacing: 0,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
String titleDesc = logic.menuVerNameArr[index];
|
||||
String imagePath =
|
||||
'mine_menu_${index + 1}.png'.mineImgPath;
|
||||
// 仅「我的消息」带未读红点,按名称判断避免菜单顺序变动后红点错位
|
||||
final isMessageEntry = titleDesc == _messageMenuTitle;
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.menuVerTapEvent(index),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
!isMessageEntry
|
||||
? Image.asset(imagePath, width: 28)
|
||||
: Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
"mine_notification.png".mineImgPath,
|
||||
width: 28,
|
||||
),
|
||||
Consumer<MineMsgProvider>(
|
||||
builder: (context, model, child) =>
|
||||
Positioned(
|
||||
right: 2,
|
||||
top: 2,
|
||||
child: model.buildTipsView(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
5.sizeBoxH,
|
||||
Text(
|
||||
titleDesc,
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// iOS 换图标需弹系统弹窗且要预置图标资源,暂不支持,仅 Android 显示该入口
|
||||
if (!Platform.isIOS) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 12, top: 10, right: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'设置桌面图标',
|
||||
style: TextStyle(
|
||||
color: Color(0xffEFEFEF),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.changeIcon(0),
|
||||
child: Text(
|
||||
'恢复默认',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
11.sizeBoxH,
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (_, __) => 7.sizeBoxW,
|
||||
itemCount: logic.iconList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final data = logic.iconList[index];
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.changeIcon(index),
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(data.image, width: 54),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
data.name,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 12,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'我的邀请码: ${store.meInfo?.promotionCode ?? ''}',
|
||||
style: textStyle(16, Color(0xffBDBDBD), FontWeight.w400),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
_buildGroundUrl(),
|
||||
30.sizeBoxH,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//永久官方地址:地址未下发时不渲染,避免点击/复制空串
|
||||
Widget _buildGroundUrl() {
|
||||
final url = Address.groundUrl;
|
||||
if (url == null || url.isEmpty) return const SizedBox.shrink();
|
||||
return GestureDetector(
|
||||
onTap: () => launchUrlToWeb(url),
|
||||
onLongPress: () {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
showToast("复制成功");
|
||||
},
|
||||
child: Text(
|
||||
'永久官方地址: $url',
|
||||
style: textStyle(12, Color(0xff525252), FontWeight.w400),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//消息和设置
|
||||
Widget _buildTop(MineMainLogic logic) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.menuTapEvent(3),
|
||||
child: Image.asset("mine_notification.png".mineImgPath, width: 24),
|
||||
),
|
||||
Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.toSetting(),
|
||||
child: Image.asset("setting.png".mineImgPath, width: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//次数
|
||||
Widget _buildCountView() {
|
||||
return Row(
|
||||
children: [
|
||||
50.sizeBoxW,
|
||||
Expanded(
|
||||
child: Consumer2<GlobalStore, PreSaleProvider>(
|
||||
builder: (context, store, provider, child) {
|
||||
int count = (store.wallet?.downloadCount ?? 0) +
|
||||
(provider.remain?.todayDownloadCount ?? 0);
|
||||
return _buildCountItem(count.toString(), '剩余缓存次数');
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
width: 0.5,
|
||||
height: 12,
|
||||
),
|
||||
Expanded(
|
||||
child: Consumer2<GlobalStore, PreSaleProvider>(
|
||||
builder: (context, store, provider, child) {
|
||||
//预售权益和免费次数的和
|
||||
final total = (store.wallet?.aiUndressFreeTimes ?? 0) +
|
||||
(provider.remain?.todayAiUndressCount ?? 0);
|
||||
return _buildCountItem(total.toString(), '剩余AI脱衣次数');
|
||||
},
|
||||
),
|
||||
),
|
||||
50.sizeBoxW,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountItem(String count, String title) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
count,
|
||||
style: textStyle(16, Colors.white, FontWeight.w500),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
title,
|
||||
style: textStyle(
|
||||
12, Colors.white.withValues(alpha: .7), FontWeight.w400),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShop(MineMainLogic logic) {
|
||||
if (Config.isStoreOpen) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.toShop(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0, left: 10, right: 10),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 355 / 82,
|
||||
child: Image.asset('mine_shop_icon.webp'.mineImgPath),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import '../../../hj_model/user/user_info_model.dart';
|
||||
|
||||
class MineIdentityLogic extends GetxController {
|
||||
MineIdentityLogic get to => Get.find<MineIdentityLogic>();
|
||||
|
||||
UserInfoModel? meInfo = globalStore.meInfo;
|
||||
|
||||
GlobalKey boundaryKey = GlobalKey();
|
||||
String qrCodeStr = '';
|
||||
|
||||
int? inviterNum;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData() async {
|
||||
globalStore.updateUserInfo();
|
||||
qrCodeStr = await MineService.certificateQR() ?? '';
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/address.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/hj_utils/image_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'mine_identity_logic.dart';
|
||||
|
||||
//账号凭证页面
|
||||
class MineAccountIdentityPage extends StatelessWidget {
|
||||
const MineAccountIdentityPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineIdentityLogic>(
|
||||
init: MineIdentityLogic(),
|
||||
builder: (controller) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
key: controller.boundaryKey,
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
child: Image.asset('share_bg.webp'.mineImgPath,
|
||||
fit: BoxFit.fill),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
30.sizeBoxH,
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, provider, __) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
border: Border.all(
|
||||
color: Color(0x4DF68804), width: 3),
|
||||
),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl:
|
||||
provider.meInfo?.portrait ?? '',
|
||||
width: 82,
|
||||
height: 82,
|
||||
borderRadius: 50,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
'账号凭证',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
'ID ${globalStore.meInfo?.uid ?? ""}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
Text("提示*可通过官方客服找回账号",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFFF68804))),
|
||||
10.sizeBoxH,
|
||||
Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(11.w),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"code_bg.webp".mineImgPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
color: Colors.white,
|
||||
child: controller.qrCodeStr.isNotEmpty
|
||||
? QrImageView(
|
||||
data: controller.qrCodeStr,
|
||||
version: QrVersions.auto,
|
||||
padding: EdgeInsets.zero,
|
||||
size: 100,
|
||||
)
|
||||
: SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Center(
|
||||
child:
|
||||
CupertinoActivityIndicator(
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
18.h.sizeBoxH,
|
||||
EasyRichText(
|
||||
"APP更新可能会导致会员账号掉线\n请勿删除此凭证,此凭证是您永久有\n效的登陆途径。",
|
||||
textAlign: TextAlign.center,
|
||||
defaultStyle: TextStyle(
|
||||
color:
|
||||
Colors.white.withValues(alpha: .55),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '请勿删除此凭证',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF52C56),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
height: 30,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 18, vertical: 3),
|
||||
child: Text(
|
||||
'${Config.appName} 官网地址 ${Address.groundUrl ?? ""}',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Colors.white.withValues(alpha: .55),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
20.sizeBoxH,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
var success = await ImageUtil.saveWidgetToAlbum(
|
||||
controller.boundaryKey);
|
||||
if (success) {
|
||||
showToast("保存成功");
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 220,
|
||||
height: 44,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"立即保存",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
20.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.back(result: false),
|
||||
child: Image.asset("close_button.png".commonImgPath,
|
||||
width: 32),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
class ApcApiModel {
|
||||
String? bank;
|
||||
bool? validated;
|
||||
String? cardType;
|
||||
String? key;
|
||||
List<dynamic>? messages;
|
||||
String? stat;
|
||||
|
||||
static ApcApiModel? fromMap(Map<String, dynamic>? map) {
|
||||
if (map == null) return null;
|
||||
ApcApiModel apcApiModel = ApcApiModel();
|
||||
apcApiModel.bank = map['bank'];
|
||||
apcApiModel.validated = map['validated'];
|
||||
apcApiModel.cardType = map['cardType'];
|
||||
apcApiModel.key = map['key'];
|
||||
apcApiModel.messages = map['messages'];
|
||||
apcApiModel.stat = map['stat'];
|
||||
return apcApiModel;
|
||||
}
|
||||
|
||||
Map toJson() => {
|
||||
"bank": bank,
|
||||
"validated": validated,
|
||||
"cardType": cardType,
|
||||
"key": key,
|
||||
"messages": messages,
|
||||
"stat": stat,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
class WithdrawConfig {
|
||||
WithdrawConfig({
|
||||
this.channels,
|
||||
this.id,
|
||||
this.cashTax,
|
||||
this.coinTax,
|
||||
this.gameTax,
|
||||
});
|
||||
|
||||
List<Channel>? channels;
|
||||
int? id;
|
||||
int? cashTax;
|
||||
int? coinTax;
|
||||
int? gameTax;
|
||||
|
||||
Channel? get getBankCardChannel {
|
||||
for (Channel item in (channels ?? [])) {
|
||||
if (item.isBankCard) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Channel> get realChannel {
|
||||
final channel_ = <Channel>[];
|
||||
|
||||
if (getBankCardChannel != null) {
|
||||
channel_.add(getBankCardChannel!);
|
||||
}
|
||||
|
||||
if (getUsdtChannel != null) {
|
||||
channel_.add(getUsdtChannel!);
|
||||
}
|
||||
|
||||
return channel_;
|
||||
}
|
||||
|
||||
Channel? get getUsdtChannel {
|
||||
for (Channel item in (channels ?? [])) {
|
||||
if (item.isUsdt) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get hasBankCard {
|
||||
for (Channel item in (channels ?? [])) {
|
||||
if (item.isBankCard) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get hasUsdt {
|
||||
for (Channel item in (channels ?? [])) {
|
||||
if (item.isUsdt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
factory WithdrawConfig.fromJson(Map<String, dynamic> json) => WithdrawConfig(
|
||||
channels: List<Channel>.from(json["channels"].map((x) => Channel.fromJson(x))),
|
||||
id: json["ID"],
|
||||
cashTax: json["cashTax"],
|
||||
coinTax: json["coinTax"],
|
||||
gameTax: json["gameTax"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"channels": channels == null ? [] : List<dynamic>.from(channels!.map((x) => x.toJson())),
|
||||
"ID": id,
|
||||
"cashTax": cashTax,
|
||||
"coinTax": coinTax,
|
||||
"gameTax": gameTax,
|
||||
};
|
||||
}
|
||||
|
||||
class Channel {
|
||||
Channel({
|
||||
this.channelName,
|
||||
this.cid,
|
||||
this.payType,
|
||||
this.minMoney,
|
||||
this.maxMoney,
|
||||
this.qpMinMoney,
|
||||
this.qpMaxMoney,
|
||||
});
|
||||
|
||||
String? channelName;
|
||||
String? cid;
|
||||
String? payType;
|
||||
int? minMoney;
|
||||
int? maxMoney;
|
||||
int? qpMinMoney;
|
||||
int? qpMaxMoney;
|
||||
bool get isBankCard => payType?.toLowerCase() == "bankcard";
|
||||
bool get isUsdt => payType?.toLowerCase() == "usdt";
|
||||
|
||||
factory Channel.fromJson(Map<String, dynamic> json) => Channel(
|
||||
channelName: json["channelName"],
|
||||
cid: json["cid"],
|
||||
payType: json["payType"],
|
||||
minMoney: json["minMoney"],
|
||||
maxMoney: json["maxMoney"],
|
||||
qpMinMoney: json["qpMinMoney"],
|
||||
qpMaxMoney: json["qpMaxMoney"],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"channelName": channelName,
|
||||
"cid": cid,
|
||||
"payType": payType,
|
||||
"minMoney": minMoney,
|
||||
"maxMoney": maxMoney,
|
||||
"qpMinMoney": qpMinMoney,
|
||||
"qpMaxMoney": qpMaxMoney,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import 'mine_withdrawal_record_page.dart';
|
||||
import 'widget/record_list_item.dart';
|
||||
import 'withdraw_details_model.dart';
|
||||
|
||||
abstract class MineWithdrawalRecordLogic extends GetxController {
|
||||
RefreshController? refreshController;
|
||||
int page = 1;
|
||||
|
||||
WithdrawDetailsModel? withdrawDetailsModel;
|
||||
final dataSource = [];
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
onReady() {
|
||||
super.onReady();
|
||||
fetchPageData();
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
if (isRefresh) page = 1;
|
||||
}
|
||||
|
||||
Widget instanceChildItem(int index);
|
||||
}
|
||||
|
||||
//提现明细
|
||||
class WithDrawalRecordController extends MineWithdrawalRecordLogic {
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
|
||||
final result =
|
||||
await MineService.getWithdrawDetails(pageNumber: page, pageSize: 10);
|
||||
isLoading = false;
|
||||
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
result?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(result?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return WithdrawalRecordItem(dataSource[index]);
|
||||
}
|
||||
}
|
||||
|
||||
//金币订单明细
|
||||
class GoldBillRecordController extends MineWithdrawalRecordLogic {
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final res = await MineService.getBillData(
|
||||
pageSize: 15,
|
||||
pageNumber: page,
|
||||
type: 1,
|
||||
);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return GoldRecordItem(dataSource[index]);
|
||||
}
|
||||
}
|
||||
|
||||
//充值明细
|
||||
class RechargeRecordController extends MineWithdrawalRecordLogic {
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final res =
|
||||
await MineService.getRechargeBill(pageNumber: page, pageSize: 15);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 14),
|
||||
child: RechargeRecordItem(dataSource[index]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//收益明细
|
||||
class IncomeRecordController extends MineWithdrawalRecordLogic {
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final res =
|
||||
await MineService.getIncomeRecord(pageNumber: page, pageSize: 15);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 12),
|
||||
child: InComeRecordItem(dataSource[index]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MineWithdrawalRecordLogic instanceController(RecordType type) {
|
||||
switch (type) {
|
||||
case RecordType.bill:
|
||||
return GoldBillRecordController();
|
||||
case RecordType.withdraw:
|
||||
return WithDrawalRecordController();
|
||||
case RecordType.recharge:
|
||||
return RechargeRecordController();
|
||||
case RecordType.income:
|
||||
return IncomeRecordController();
|
||||
default:
|
||||
throw '$type 没有找到';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import 'mine_withdrawal_record_logic.dart';
|
||||
|
||||
enum RecordType {
|
||||
recharge('充值记录'),
|
||||
withdraw('提现明细'),
|
||||
bill('余额明细'),
|
||||
income('业绩明细');
|
||||
|
||||
final String title;
|
||||
const RecordType(this.title);
|
||||
}
|
||||
|
||||
//明细综合页面
|
||||
class RecordsPage extends StatefulWidget {
|
||||
final RecordType type; // 0:收益, 1: 提现
|
||||
const RecordsPage(this.type, {super.key});
|
||||
|
||||
@override
|
||||
State<RecordsPage> createState() => _RecordsPageState();
|
||||
}
|
||||
|
||||
class _RecordsPageState extends State<RecordsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineWithdrawalRecordLogic>(
|
||||
init: instanceController(widget.type),
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.type.title)),
|
||||
body: pullYsRefresh(
|
||||
onRefresh: (refreshController) => controller.fetchPageData(),
|
||||
onLoading: (refreshController) =>
|
||||
controller.fetchPageData(isRefresh: false),
|
||||
onInit: (ctr) => controller.refreshController = ctr,
|
||||
child: () {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
if (controller.dataSource.isEmpty)
|
||||
return CErrorWidget(
|
||||
retryOnTap: () => controller.fetchPageData());
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: controller.dataSource.length,
|
||||
padding: EdgeInsets.only(top: 12, left: 16.w, right: 16.w),
|
||||
itemBuilder: (context, index) =>
|
||||
controller.instanceChildItem(index),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/mine/exchange/bill_item_model.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../withdraw_details_model.dart';
|
||||
|
||||
class GoldRecordItem extends StatelessWidget {
|
||||
final BillItemModel model;
|
||||
|
||||
const GoldRecordItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
model.tranType ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
model.desc ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .5),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
model.createdAt.utcToYMDHMS(),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .5),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
Text(
|
||||
'${model.realCount}${model.unit}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF68804),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
12.sizeBoxH,
|
||||
1.line,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//充值记录
|
||||
class RechargeRecordItem extends StatelessWidget {
|
||||
final ListBean model;
|
||||
|
||||
const RechargeRecordItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"${model.productName ?? ""}",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
///复制到剪切板
|
||||
Clipboard.setData(ClipboardData(text: model.orderId ?? ''));
|
||||
showToast('复制成功');
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'账单编号' + ': ${model.orderId}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
2.sizeBoxW,
|
||||
Image.asset(
|
||||
'icon_copy.png'.mineImgPath,
|
||||
width: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
"状态: ${getStatus(model.status ?? 0)}",
|
||||
style: TextStyle(
|
||||
color: Color(0xffF68804),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
model.createdAt?.utcToYMD() ?? "",
|
||||
style: TextStyle(fontSize: 12, color: Color(0xff525252)),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
0.5.line,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///支付状态
|
||||
String getStatus(int status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return "进行中";
|
||||
case 2:
|
||||
return "购买失败";
|
||||
case 3:
|
||||
return "购买成功";
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
Color getStatusColor(int status) {
|
||||
var statusStr = Color(0xffffd382);
|
||||
switch (status) {
|
||||
case 2:
|
||||
statusStr = Color(0xffFF1060);
|
||||
break;
|
||||
case 3:
|
||||
statusStr = Color(0xff28C445);
|
||||
break;
|
||||
}
|
||||
|
||||
return statusStr;
|
||||
}
|
||||
}
|
||||
|
||||
class InComeRecordItem extends StatelessWidget {
|
||||
final IncomeModel model;
|
||||
const InComeRecordItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Column(children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${model.tranType}",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFEFEFEF),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.0),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
model.desc ?? "",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white60),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateTimeUtil.utc2iso(model.createdAt ?? ''),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF525252),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Text(
|
||||
model.tranTypeInt == 111
|
||||
? "+${model.actualAmount}次"
|
||||
: "+${model.actualAmount}金币",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xFFF68804),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
textAlign: TextAlign.left)
|
||||
],
|
||||
),
|
||||
0.5.line,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
//提现明细cell
|
||||
class WithdrawalRecordItem extends StatefulWidget {
|
||||
final ListBean model;
|
||||
const WithdrawalRecordItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
State<WithdrawalRecordItem> createState() => _WithdrawalRecordItemState();
|
||||
}
|
||||
|
||||
class _WithdrawalRecordItemState extends State<WithdrawalRecordItem> {
|
||||
ListBean get model => widget.model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'${(model.money ?? 0) ~/ 100}元',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.actionRed,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'账单编号' + ': ${model.id}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
///复制到剪切板
|
||||
Clipboard.setData(ClipboardData(text: model.id ?? ''));
|
||||
showToast('复制成功');
|
||||
},
|
||||
child: Image.asset(
|
||||
'mine_copy.png'.mineImgPath,
|
||||
width: 24,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Text(
|
||||
'${getPayType(model.payType ?? "")}${getStatus(model.status ?? 0)}',
|
||||
style: TextStyle(fontSize: 12, color: getStatusColor()),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
DateTimeUtil.utc2iso(model.createdAt ?? ''),
|
||||
style: TextStyle(fontSize: 12, color: Color(0x8CFFFFFF)),
|
||||
),
|
||||
Spacer(),
|
||||
if (model.status != 5 && model.status != 1) ...[
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => setState(() {
|
||||
model.showReason = !model.showReason;
|
||||
}),
|
||||
child: Text(
|
||||
'查看原因',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.actionRed),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
if (model.showReason) ...[
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'${model.statusDesc}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
],
|
||||
12.sizeBoxH,
|
||||
0.5.line,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getPayType(String payType) {
|
||||
if (payType.endsWith("alipay")) {
|
||||
return "支付宝";
|
||||
} else if (payType.endsWith("usdt")) {
|
||||
return "USDT";
|
||||
} else {
|
||||
return "银行卡";
|
||||
}
|
||||
}
|
||||
|
||||
///支付状态
|
||||
String getStatus(int status) {
|
||||
switch (status) {
|
||||
case 1:
|
||||
return '提现审核中';
|
||||
case 2:
|
||||
return '审核通过,转账中';
|
||||
case 3:
|
||||
return '提现已拒绝';
|
||||
case 4:
|
||||
return '未知错误';
|
||||
case 5:
|
||||
return '提现成功';
|
||||
case 6:
|
||||
return '提现失败';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
///支付状态
|
||||
Color getStatusColor() {
|
||||
if (model.status == 5) {
|
||||
return Color(0xff0360FC);
|
||||
} else {
|
||||
return Color(0x8CFFFFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/// hasNext : true
|
||||
|
||||
class WithdrawDetailsModel {
|
||||
bool? hasNext;
|
||||
List<ListBean>? list;
|
||||
List<ResultBean>? result;
|
||||
int? total;
|
||||
|
||||
static WithdrawDetailsModel? fromJson(Map<String, dynamic> map) {
|
||||
WithdrawDetailsModel withdrawDetailsModel = WithdrawDetailsModel();
|
||||
withdrawDetailsModel.hasNext = map['hasNext'];
|
||||
withdrawDetailsModel.list = []..addAll((map['list'] as List? ?? []).map((o) => ListBean.fromMap(o)));
|
||||
withdrawDetailsModel.result = []..addAll((map['result'] as List? ?? []).map((o) => ResultBean.fromMap(o)));
|
||||
withdrawDetailsModel.total = map['total'];
|
||||
return withdrawDetailsModel;
|
||||
}
|
||||
|
||||
Map toJson() => {
|
||||
"hasNext": hasNext,
|
||||
"list": list,
|
||||
"result": result,
|
||||
"total": total,
|
||||
};
|
||||
}
|
||||
|
||||
class ResultBean {
|
||||
int? uid;
|
||||
int? amount;
|
||||
int? money;
|
||||
int? payMoney;
|
||||
int? withdrawType;
|
||||
int? status;
|
||||
String? id;
|
||||
String? name;
|
||||
String? oid;
|
||||
String? payType;
|
||||
String? actName;
|
||||
String? act;
|
||||
String? userIp;
|
||||
String? deviceType;
|
||||
String? devID;
|
||||
String? statusDesc;
|
||||
String? checkedAt;
|
||||
String? progressAt;
|
||||
String? failureAt;
|
||||
String? successAt;
|
||||
String? updatedAt;
|
||||
String? createdAt;
|
||||
String? receivedAt;
|
||||
|
||||
static ResultBean fromMap(Map<String, dynamic> map) {
|
||||
ResultBean info = ResultBean();
|
||||
info.id = map['id'];
|
||||
info.uid = map['uid'];
|
||||
info.name = map['name'];
|
||||
info.amount = map['amount'];
|
||||
info.oid = map['oid'];
|
||||
info.money = map['money'];
|
||||
info.payMoney = map['payMoney'];
|
||||
info.payType = map['payType'];
|
||||
info.withdrawType = map['withdrawType'];
|
||||
info.actName = map['actName'];
|
||||
info.act = map['act'];
|
||||
info.userIp = map['userIp'];
|
||||
info.deviceType = map['deviceType'];
|
||||
info.devID = map['devID'];
|
||||
info.status = map['status'];
|
||||
info.statusDesc = map['statusDesc'];
|
||||
info.checkedAt = map['checkedAt'];
|
||||
info.progressAt = map['progressAt'];
|
||||
info.failureAt = map['failureAt'];
|
||||
info.successAt = map['successAt'];
|
||||
info.updatedAt = map['updatedAt'];
|
||||
info.createdAt = map['createdAt'];
|
||||
info.receivedAt = map['receivedAt'];
|
||||
return info;
|
||||
}
|
||||
|
||||
Map toJson() => {
|
||||
"id": id,
|
||||
"uid": uid,
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"oid": oid,
|
||||
"money": money,
|
||||
"payMoney": payMoney,
|
||||
"payType": payType,
|
||||
"withdrawType": withdrawType,
|
||||
"actName": actName,
|
||||
"act": act,
|
||||
"userIp": userIp,
|
||||
"deviceType": deviceType,
|
||||
"devID": devID,
|
||||
"status": status,
|
||||
"statusDesc": statusDesc,
|
||||
"checkedAt": checkedAt,
|
||||
"progressAt": progressAt,
|
||||
"failureAt": failureAt,
|
||||
"successAt": successAt,
|
||||
"updatedAt": updatedAt,
|
||||
"createdAt": createdAt,
|
||||
"receivedAt": receivedAt,
|
||||
};
|
||||
}
|
||||
|
||||
class ListBean {
|
||||
int? money;
|
||||
int? payMoney;
|
||||
int? uid;
|
||||
int? amount;
|
||||
int? withdrawType;
|
||||
int? status;
|
||||
String? id;
|
||||
String? orderId;
|
||||
String? name;
|
||||
String? oid;
|
||||
String? payType;
|
||||
String? actName;
|
||||
String? act;
|
||||
String? userIp;
|
||||
String? deviceType;
|
||||
String? devID;
|
||||
String? statusDesc;
|
||||
String? checkedAt;
|
||||
String? progressAt;
|
||||
String? failureAt;
|
||||
String? successAt;
|
||||
String? updatedAt;
|
||||
String? createdAt;
|
||||
String? desc;
|
||||
String? receivedAt;
|
||||
String? productName;
|
||||
double? actualAmount;
|
||||
bool showReason = false; //本地添加字段,是否展示原因
|
||||
|
||||
static ListBean fromMap(Map<String, dynamic> map) {
|
||||
ListBean info = ListBean();
|
||||
info.id = map['id'];
|
||||
info.orderId = map['orderId'];
|
||||
info.uid = map['uid'];
|
||||
info.name = map['name'];
|
||||
info.amount = map['amount'];
|
||||
info.oid = map['oid'];
|
||||
info.money = map['money'];
|
||||
info.payMoney = map['payMoney'];
|
||||
info.payType = map['payType'];
|
||||
info.withdrawType = map['withdrawType'];
|
||||
info.actName = map['actName'];
|
||||
info.act = map['act'];
|
||||
info.userIp = map['userIp'];
|
||||
info.deviceType = map['deviceType'];
|
||||
info.devID = map['devID'];
|
||||
info.status = map['status'];
|
||||
info.statusDesc = map['statusDesc'];
|
||||
info.checkedAt = map['checkedAt'];
|
||||
info.progressAt = map['progressAt'];
|
||||
info.failureAt = map['failureAt'];
|
||||
info.successAt = map['successAt'];
|
||||
info.updatedAt = map['updatedAt'];
|
||||
info.createdAt = map['createdAt'];
|
||||
info.receivedAt = map['receivedAt'];
|
||||
info.desc = map['desc'];
|
||||
info.actualAmount = map['actualAmount']?.toDouble() ?? .0;
|
||||
info.productName = map['productName'];
|
||||
return info;
|
||||
}
|
||||
|
||||
Map toJson() => {
|
||||
"id": id,
|
||||
"uid": uid,
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"oid": oid,
|
||||
"money": money,
|
||||
"payMoney": payMoney,
|
||||
"payType": payType,
|
||||
"withdrawType": withdrawType,
|
||||
"actName": actName,
|
||||
"act": act,
|
||||
"userIp": userIp,
|
||||
"deviceType": deviceType,
|
||||
"devID": devID,
|
||||
"status": status,
|
||||
"statusDesc": statusDesc,
|
||||
"checkedAt": checkedAt,
|
||||
"progressAt": progressAt,
|
||||
"failureAt": failureAt,
|
||||
"successAt": successAt,
|
||||
"updatedAt": updatedAt,
|
||||
"createdAt": createdAt,
|
||||
"receivedAt": receivedAt,
|
||||
"desc": desc,
|
||||
"actualAmount": actualAmount,
|
||||
};
|
||||
}
|
||||
|
||||
class IncomeModel {
|
||||
String? id;
|
||||
int? uid;
|
||||
String? purchaseOrder;
|
||||
String? productID;
|
||||
num? amount;
|
||||
num? integral;
|
||||
String? realIntegral;
|
||||
num? actualIntegral;
|
||||
num? actualAmount;
|
||||
num? tax;
|
||||
num? taxAmount;
|
||||
String? channelType;
|
||||
String? tranType;
|
||||
num? tranTypeInt;
|
||||
num? performance;
|
||||
num? rechargeId;
|
||||
RechargeUser? rechargeUser;
|
||||
String? desc;
|
||||
String? createdAt;
|
||||
String? sysType;
|
||||
num? agentLevel;
|
||||
num? vipLevel;
|
||||
String? realAmount;
|
||||
String? money;
|
||||
String? wlRealAmount;
|
||||
num? fruitCoin;
|
||||
num? downloadCount;
|
||||
num? fruitCoinBalance;
|
||||
num? aiMateBalance;
|
||||
String? districtCode;
|
||||
String? promSeqe;
|
||||
bool? isDirect;
|
||||
String? discBindAt;
|
||||
|
||||
IncomeModel(
|
||||
{this.id,
|
||||
this.uid,
|
||||
this.purchaseOrder,
|
||||
this.productID,
|
||||
this.amount,
|
||||
this.integral,
|
||||
this.realIntegral,
|
||||
this.actualIntegral,
|
||||
this.actualAmount,
|
||||
this.tax,
|
||||
this.taxAmount,
|
||||
this.channelType,
|
||||
this.tranType,
|
||||
this.tranTypeInt,
|
||||
this.performance,
|
||||
this.rechargeId,
|
||||
this.rechargeUser,
|
||||
this.desc,
|
||||
this.createdAt,
|
||||
this.sysType,
|
||||
this.agentLevel,
|
||||
this.vipLevel,
|
||||
this.realAmount,
|
||||
this.money,
|
||||
this.wlRealAmount,
|
||||
this.fruitCoin,
|
||||
this.downloadCount,
|
||||
this.fruitCoinBalance,
|
||||
this.aiMateBalance,
|
||||
this.districtCode,
|
||||
this.promSeqe,
|
||||
this.isDirect,
|
||||
this.discBindAt});
|
||||
|
||||
handelMoney() {
|
||||
double price = (actualAmount ?? 0) / 10;
|
||||
return price.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
IncomeModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
uid = json['uid'];
|
||||
purchaseOrder = json['purchaseOrder'];
|
||||
productID = json['productID'];
|
||||
amount = json['amount'];
|
||||
integral = json['integral'];
|
||||
realIntegral = json['realIntegral'];
|
||||
actualIntegral = json['actualIntegral'];
|
||||
actualAmount = json['actualAmount'];
|
||||
tax = json['tax'];
|
||||
taxAmount = json['taxAmount'];
|
||||
channelType = json['channelType'];
|
||||
tranType = json['tranType'];
|
||||
tranTypeInt = json['tranTypeInt'];
|
||||
performance = json['performance'];
|
||||
rechargeId = json['rechargeId'];
|
||||
rechargeUser = json['rechargeUser'] != null ? new RechargeUser.fromJson(json['rechargeUser']) : null;
|
||||
desc = json['desc'];
|
||||
createdAt = json['createdAt'];
|
||||
sysType = json['sysType'];
|
||||
agentLevel = json['agentLevel'];
|
||||
vipLevel = json['vipLevel'];
|
||||
realAmount = json['realAmount'];
|
||||
money = json['money'];
|
||||
wlRealAmount = json['wlRealAmount'];
|
||||
fruitCoin = json['fruitCoin'];
|
||||
downloadCount = json['downloadCount'];
|
||||
fruitCoinBalance = json['fruitCoinBalance'];
|
||||
aiMateBalance = json['aiMateBalance'];
|
||||
districtCode = json['districtCode'];
|
||||
promSeqe = json['promSeqe'];
|
||||
isDirect = json['isDirect'];
|
||||
discBindAt = json['DiscBindAt'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['id'] = this.id;
|
||||
data['uid'] = this.uid;
|
||||
data['purchaseOrder'] = this.purchaseOrder;
|
||||
data['productID'] = this.productID;
|
||||
data['amount'] = this.amount;
|
||||
data['integral'] = this.integral;
|
||||
data['realIntegral'] = this.realIntegral;
|
||||
data['actualIntegral'] = this.actualIntegral;
|
||||
data['actualAmount'] = this.actualAmount;
|
||||
data['tax'] = this.tax;
|
||||
data['taxAmount'] = this.taxAmount;
|
||||
data['channelType'] = this.channelType;
|
||||
data['tranType'] = this.tranType;
|
||||
data['tranTypeInt'] = this.tranTypeInt;
|
||||
data['performance'] = this.performance;
|
||||
data['rechargeId'] = this.rechargeId;
|
||||
if (this.rechargeUser != null) {
|
||||
data['rechargeUser'] = this.rechargeUser!.toJson();
|
||||
}
|
||||
data['desc'] = this.desc;
|
||||
data['createdAt'] = this.createdAt;
|
||||
data['sysType'] = this.sysType;
|
||||
data['agentLevel'] = this.agentLevel;
|
||||
data['vipLevel'] = this.vipLevel;
|
||||
data['realAmount'] = this.realAmount;
|
||||
data['money'] = this.money;
|
||||
data['wlRealAmount'] = this.wlRealAmount;
|
||||
data['fruitCoin'] = this.fruitCoin;
|
||||
data['downloadCount'] = this.downloadCount;
|
||||
data['fruitCoinBalance'] = this.fruitCoinBalance;
|
||||
data['aiMateBalance'] = this.aiMateBalance;
|
||||
data['districtCode'] = this.districtCode;
|
||||
data['promSeqe'] = this.promSeqe;
|
||||
data['isDirect'] = this.isDirect;
|
||||
data['DiscBindAt'] = this.discBindAt;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RechargeUser {
|
||||
int? uid;
|
||||
String? name;
|
||||
String? portrait;
|
||||
|
||||
RechargeUser({this.uid, this.name, this.portrait});
|
||||
|
||||
RechargeUser.fromJson(Map<String, dynamic> json) {
|
||||
uid = json['uid'];
|
||||
name = json['name'];
|
||||
portrait = json['portrait'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['uid'] = this.uid;
|
||||
data['name'] = this.name;
|
||||
data['portrait'] = this.portrait;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/user/wallet_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
|
||||
import '../../../alert/mine/vip_level_dialog.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import '../../../track_event_manager/device_service.dart';
|
||||
import '../mine_profit/bank_card_home_page.dart';
|
||||
import '../mine_profit/model/alipay_bank_list_model.dart';
|
||||
import 'in_come_entity.dart';
|
||||
|
||||
class WithdrawalLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
WithdrawalLogic get to => Get.find<WithdrawalLogic>();
|
||||
|
||||
TextEditingController? moneyController;
|
||||
TextEditingController? accountController;
|
||||
FocusNode focusNode = FocusNode();
|
||||
bool isShowLoading = false;
|
||||
|
||||
WalletModel? userIncomeModel;
|
||||
WithdrawConfig? configData;
|
||||
TextEditingController? nameController;
|
||||
bool isLoading = true;
|
||||
|
||||
int withdrawType = 1; // 0支付宝 1银行卡
|
||||
Channel? selectChannel;
|
||||
|
||||
num handlingFee = 0; //手续费
|
||||
num actualAmount = 0; //实际到账金额
|
||||
AccountInfoModel? bankModel;
|
||||
int get minBankCardMoney {
|
||||
return (configData?.getBankCardChannel?.minMoney ?? 0) ~/ 100;
|
||||
}
|
||||
|
||||
int get minUsdtMoney {
|
||||
return (configData?.getUsdtChannel?.minMoney ?? 0) ~/ 100;
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
moneyController = TextEditingController();
|
||||
nameController = TextEditingController();
|
||||
accountController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData(showLoading: true);
|
||||
globalStore.refreshWallet();
|
||||
}
|
||||
|
||||
void changeWithdrawType(String payType) {
|
||||
// withdrawType = value;
|
||||
selectChannel = configData?.channels
|
||||
?.firstWhere((element) => element.payType == payType);
|
||||
withdrawType = configData?.channels
|
||||
?.indexWhere((e) => e.payType == selectChannel?.payType) ??
|
||||
0;
|
||||
update();
|
||||
}
|
||||
|
||||
///计算提现手续费、实际到账金额
|
||||
void calcWithdrawAmount(String withdrawAmount) {
|
||||
if (withdrawAmount.isEmpty) {
|
||||
handlingFee = 0;
|
||||
actualAmount = 0;
|
||||
} else {
|
||||
num withdrawAmoutNum = num.parse(withdrawAmount);
|
||||
int coinTax = configData?.coinTax ?? 0;
|
||||
if (coinTax == 0) {
|
||||
handlingFee = 0;
|
||||
actualAmount = withdrawAmoutNum;
|
||||
} else {
|
||||
double value = coinTax / 100.0;
|
||||
final res = (withdrawAmoutNum * double.parse(value.toStringAsFixed(2)))
|
||||
.toStringAsFixed(2);
|
||||
handlingFee = double.parse(res).floor();
|
||||
actualAmount = withdrawAmoutNum - handlingFee;
|
||||
}
|
||||
}
|
||||
update(['money']);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
nameController?.dispose();
|
||||
moneyController?.dispose();
|
||||
accountController?.dispose();
|
||||
focusNode.dispose();
|
||||
}
|
||||
|
||||
Future<void> loadData({
|
||||
bool isRefresh = true,
|
||||
bool showLoading = false,
|
||||
}) async {
|
||||
//获取支付宝或者银行卡的费率
|
||||
_withdrawConfigReq();
|
||||
}
|
||||
|
||||
///提现配置请求
|
||||
void _withdrawConfigReq() async {
|
||||
final configData = await MineService.withdrawConfig();
|
||||
this.configData = configData;
|
||||
isLoading = false;
|
||||
// 优先银行卡,其次 usdt
|
||||
selectChannel = configData?.channels
|
||||
?.firstWhereOrNull((element) => element.payType == 'bankcard') ??
|
||||
configData?.channels
|
||||
?.firstWhereOrNull((element) => element.payType == 'usdt');
|
||||
if (selectChannel == null) {
|
||||
showToast('暂无可用提现通道');
|
||||
this.configData = null;
|
||||
} else {
|
||||
// 必须同步 withdrawType,页面按 channels[withdrawType] 取值,否则会索引错位甚至越界
|
||||
withdrawType = configData?.channels
|
||||
?.indexWhere((e) => e.payType == selectChannel?.payType) ??
|
||||
0;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
///提交提现
|
||||
void submitWithdraw() async {
|
||||
try {
|
||||
if (!globalStore.isRechargeVIP) {
|
||||
showVipLevelDialog("您还不是VIP,无法使用提现功能");
|
||||
return;
|
||||
}
|
||||
|
||||
if ("bankcard" == selectChannel?.payType) {
|
||||
//检验银行卡信息
|
||||
_commonWithdrawReq(true);
|
||||
} else if ("alipay" == selectChannel?.payType ||
|
||||
"usdt" == selectChannel?.payType) {
|
||||
_commonWithdrawReq(false);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("提现错误:$e");
|
||||
}
|
||||
}
|
||||
|
||||
///公用提现方法
|
||||
void _commonWithdrawReq(bool isbankType) async {
|
||||
Channel channel = configData!.channels![withdrawType];
|
||||
String money = moneyController?.text.trim() ?? "";
|
||||
String accountDesc = accountController?.text.trim() ?? "";
|
||||
if (isbankType) {
|
||||
accountDesc = bankModel?.act ?? "";
|
||||
}
|
||||
if (money.isEmpty) {
|
||||
showToast("提现金额不能为空");
|
||||
return;
|
||||
}
|
||||
if (isbankType && bankModel == null) {
|
||||
showToast("请选择提现银行卡号");
|
||||
return;
|
||||
}
|
||||
if (!isbankType && accountDesc.isEmpty) {
|
||||
showToast("请输入钱包地址");
|
||||
return;
|
||||
}
|
||||
|
||||
double incomeMoneyYuan = (userIncomeModel?.balance ?? 0) / 10;
|
||||
int withdrawMoneyYuan = int.parse(money);
|
||||
if (withdrawMoneyYuan > incomeMoneyYuan) {
|
||||
showToast("提现金额不能大于余额");
|
||||
return;
|
||||
}
|
||||
|
||||
int minMoneyFen = configData?.channels![withdrawType].minMoney ?? 0;
|
||||
double minMoneyYuan = minMoneyFen / 100;
|
||||
if (withdrawMoneyYuan < minMoneyYuan) {
|
||||
showToast("单笔提现金额不小于$minMoneyYuan元");
|
||||
return;
|
||||
}
|
||||
int maxMoneyFen = configData?.channels![withdrawType].maxMoney ?? 0;
|
||||
double maxMoneyYuan = maxMoneyFen / 100;
|
||||
if (withdrawMoneyYuan > maxMoneyYuan) {
|
||||
showToast("单笔提现金额不大于$maxMoneyYuan元");
|
||||
return;
|
||||
}
|
||||
|
||||
LoadingHelper.showLoading();
|
||||
|
||||
String deviceId = DeviceInfoService.deviceId;
|
||||
String payType = channel.payType ?? "";
|
||||
//payType 提现方式,alipay,bankcard,usdt
|
||||
//money 提现金额
|
||||
//name 用户名
|
||||
//withdrawType 提现类型,0,代理提现; 1,金币提现
|
||||
//actName 交易账户持有人
|
||||
//act 交易账户
|
||||
//devID 设备id
|
||||
//productType 产品类型 0站群 1棋牌
|
||||
var result = await MineService.withdraw(
|
||||
payType,
|
||||
accountDesc,
|
||||
withdrawMoneyYuan * 100,
|
||||
globalStore.meInfo?.name ?? "",
|
||||
bankModel?.actName,
|
||||
deviceId,
|
||||
bankModel?.bankCode,
|
||||
1,
|
||||
0,
|
||||
);
|
||||
LoadingHelper.dismissLoading();
|
||||
if (result == true) {
|
||||
globalStore.refreshWallet(refresh: true);
|
||||
showToast("提现提交成功~");
|
||||
|
||||
clearInputData();
|
||||
} else {
|
||||
showToast("提现失败~");
|
||||
}
|
||||
}
|
||||
|
||||
void gotoBankList() async {
|
||||
var ret = await Get.to(
|
||||
BankCardHomePage(selectModel: bankModel),
|
||||
preventDuplicates: false,
|
||||
);
|
||||
if (ret is AccountInfoModel) {
|
||||
bankModel = ret;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
clearInputData() {
|
||||
nameController?.clear();
|
||||
accountController?.clear();
|
||||
moneyController?.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/mine/widgets/gradient_text.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../assets_tool/app_colors.dart';
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import 'in_come_entity.dart';
|
||||
import 'mine_withdrawal_record_page.dart';
|
||||
import 'withdrawal_logic.dart';
|
||||
|
||||
//我要提现页面
|
||||
class WithdrawalPage extends StatelessWidget {
|
||||
const WithdrawalPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<WithdrawalLogic>(
|
||||
init: WithdrawalLogic(),
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'我要提现',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
GestureDetector(
|
||||
child: Text(
|
||||
'明细',
|
||||
style: TextStyle(color: Color(0x73FFFFFF), fontSize: 12),
|
||||
),
|
||||
onTap: () => Get.to(RecordsPage(RecordType.withdraw)),
|
||||
),
|
||||
16.w.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: () {
|
||||
if (logic.isLoading) return LoadingCenterWidget();
|
||||
if (logic.configData == null) return CErrorWidget();
|
||||
const measureStyle = TextStyle(fontSize: 14);
|
||||
var textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: '',
|
||||
style: measureStyle,
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
textWidthBasis: TextWidthBasis.longestLine,
|
||||
)..layout();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 16.w, right: 16.w),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin:
|
||||
EdgeInsets.only(top: 12.w, bottom: 18.w),
|
||||
padding: EdgeInsets.symmetric(vertical: 12.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 22,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(11),
|
||||
bottomRight: Radius.circular(11),
|
||||
),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Text(
|
||||
"余额(元)",
|
||||
style: const TextStyle(
|
||||
color: AppColors.actionRed,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
'coin_icon.webp'.mineImgPath,
|
||||
width: 30),
|
||||
4.sizeBoxW,
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, store, __) {
|
||||
logic.userIncomeModel =
|
||||
store.wallet;
|
||||
return GradientText(
|
||||
((store.wallet?.balance ?? 0) *
|
||||
10 /
|
||||
100)
|
||||
.toStringAsFixed(2),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xffFFE8BE),
|
||||
Color(0xffE6B764)
|
||||
],
|
||||
tileMode: TileMode.mirror,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 31.5,
|
||||
fontWeight: FontWeight
|
||||
.w500), //目前fontSize设置为31.5 设置为32,显示不完全,存在系统bug
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'提现币类:',
|
||||
style: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
),
|
||||
15.sizeBoxW,
|
||||
Text(
|
||||
'人民币',
|
||||
style: textStyle(
|
||||
14, Color(0x8CFFFFFF), FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"提现金额:",
|
||||
style: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 40,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: TextField(
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp("[0-9]")),
|
||||
LengthLimitingTextInputFormatter(9),
|
||||
],
|
||||
controller: logic.moneyController,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
cursorColor:
|
||||
Colors.white.withValues(alpha: 0.7),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding:
|
||||
EdgeInsets.only(bottom: 8),
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xFF525252),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400),
|
||||
hintText:
|
||||
'单笔提现金额范围 ${_getPayMoneyRange(logic)}元', //"您目前
|
||||
),
|
||||
focusNode: logic.focusNode,
|
||||
onChanged: (value) =>
|
||||
logic.calcWithdrawAmount(value)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"提现方式:",
|
||||
style: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: Row(
|
||||
children: logic.configData?.realChannel
|
||||
.asMap()
|
||||
.map((index, e) => MapEntry(
|
||||
index,
|
||||
_buildPayTypeUI(
|
||||
e, logic, index)))
|
||||
.values
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
if (_isUsdt(logic)) ...[
|
||||
Text(
|
||||
'USDT地址:',
|
||||
style: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: TextField(
|
||||
textAlignVertical:
|
||||
TextAlignVertical.center,
|
||||
controller: logic.accountController,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: _getHintText(logic),
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xff999999),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical:
|
||||
(32 - textPainter.height) / 2,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
] else ...[
|
||||
Text(
|
||||
'银行卡号:',
|
||||
style: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.gotoBankList(),
|
||||
child: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 37,
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
logic.bankModel == null
|
||||
? "请选择提现银行账号"
|
||||
: "${logic.bankModel?.getBankName()}(${logic.bankModel?.act?.substring((logic.bankModel?.act?.length ?? 4) - 4)}) ${logic.bankModel?.actName}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: logic.bankModel == null
|
||||
? Color(0xff525252)
|
||||
: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
6.sizeBoxW,
|
||||
Icon(Icons.keyboard_arrow_right,
|
||||
color: Colors.white, size: 16),
|
||||
10.sizeBoxW,
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
]
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
GetBuilder<WithdrawalLogic>(
|
||||
init: logic,
|
||||
id: 'money',
|
||||
builder: (controller) => EasyRichText(
|
||||
'手续费率: ${logic.configData?.coinTax ?? 0}% 实际到账金额: ${logic.actualAmount}',
|
||||
defaultStyle: textStyle(
|
||||
14, Color(0xE5FFFFFF), FontWeight.w500),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString:
|
||||
'${logic.configData?.coinTax ?? 0}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0x8CFFFFFF),
|
||||
),
|
||||
),
|
||||
EasyRichTextPattern(
|
||||
targetString:
|
||||
'实际到账金额: ${logic.handlingFee}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0x8CFFFFFF),
|
||||
),
|
||||
),
|
||||
EasyRichTextPattern(
|
||||
targetString: '${logic.actualAmount}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0x8CFFFFFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Text(
|
||||
"提现规则:",
|
||||
style: const TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
"1、每次提现金额最低${(logic.configData?.channels == null ? 0 : logic.selectChannel?.minMoney ?? 0) ~/ 100}元起,"
|
||||
"单笔提现最大${(logic.configData?.channels == null ? 0 : logic.selectChannel?.maxMoney ?? 0) ~/ 100}元,且为整数。\n"
|
||||
"2、每次提现收取${logic.configData?.coinTax}%手续费。\n"
|
||||
"3、仅支持银行卡提现,收款账户卡号与姓名一致,到账时间未72小时内。\n"
|
||||
"4、申请提现后请随时关注收款账户进款通知,长时间未到账,请及时联系客服。\n",
|
||||
style: const TextStyle(
|
||||
color: Color(0x8CFFFFFF),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0,
|
||||
height: 2),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => logic.submitWithdraw(),
|
||||
child: Container(
|
||||
height: 47,
|
||||
alignment: Alignment.center,
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'立即提现',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
)),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
EasyRichText(
|
||||
'提现中如有问题,请联系 在线客服',
|
||||
defaultStyle:
|
||||
textStyle(12, Colors.white, FontWeight.w400),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '在线客服',
|
||||
style: TextStyle(color: Color(0xffFFD460)),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = pushToCustomService,
|
||||
),
|
||||
],
|
||||
),
|
||||
34.sizeBoxH,
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
///支付类型
|
||||
Widget _buildPayTypeUI(Channel channel, WithdrawalLogic logic, int index) {
|
||||
bool isSelected = logic.selectChannel?.payType == channel.payType;
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.changeWithdrawType(channel.payType ?? ''),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
channel.payType == 'usdt'
|
||||
? 'ic_usdt.png'.mineImgPath
|
||||
: 'ic_union.png'.mineImgPath,
|
||||
width: 30),
|
||||
4.sizeBoxW,
|
||||
Text(_getPayTypeName(channel.payType ?? ""),
|
||||
style: TextStyle(color: Colors.white, fontSize: 14)),
|
||||
10.sizeBoxW,
|
||||
Image.asset(
|
||||
isSelected
|
||||
? 'radio_sel.png'.commonImgPath
|
||||
: 'mine_withdraw_nor.png'.mineImgPath,
|
||||
width: 24,
|
||||
),
|
||||
24.sizeBoxW,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///获取支付方式名称
|
||||
String _getPayTypeName(String payType) {
|
||||
if ("alipay" == payType) {
|
||||
return "支付宝";
|
||||
} else if ("bankcard" == payType) {
|
||||
return "银行卡";
|
||||
} else if ("usdt" == payType) {
|
||||
return "USDT";
|
||||
}
|
||||
return "银行卡";
|
||||
}
|
||||
|
||||
///获取提现金额范围
|
||||
String _getPayMoneyRange(WithdrawalLogic logic) {
|
||||
final channel = _currentChannel(logic);
|
||||
if (channel == null) return "0";
|
||||
return "${(channel.minMoney ?? 0) / 100}-${(channel.maxMoney ?? 0) / 100}";
|
||||
}
|
||||
|
||||
String _getHintText(WithdrawalLogic logic) {
|
||||
final channel = _currentChannel(logic);
|
||||
switch (channel?.payType ?? "") {
|
||||
case 'alipay':
|
||||
return '请输入支付宝账号';
|
||||
case 'bankcard':
|
||||
return '请输入银行卡号';
|
||||
case 'usdt':
|
||||
return '请输入USDT地址';
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
bool _isUsdt(WithdrawalLogic logic) =>
|
||||
(_currentChannel(logic)?.payType ?? "") == 'usdt';
|
||||
|
||||
// 按 withdrawType 取当前通道,带越界保护(配置异常时不崩溃)
|
||||
Channel? _currentChannel(WithdrawalLogic logic) {
|
||||
final channels = logic.configData?.channels ?? [];
|
||||
final index = logic.withdrawType;
|
||||
if (index < 0 || index >= channels.length) return null;
|
||||
return channels[index];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../hj_utils/api_service/mine_service.dart';
|
||||
|
||||
class MessageCenterLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
MessageCenterLogic get to => Get.find<MessageCenterLogic>();
|
||||
|
||||
late TabController tabController = TabController(length: titleArr.length, vsync: this);
|
||||
|
||||
List<String> titleArr = ['评论', '点赞'];
|
||||
|
||||
void getUnReadNum() async {
|
||||
await MineService.getUnreadNum();
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import 'message_center_logic.dart';
|
||||
import 'message_center_sub_page.dart';
|
||||
|
||||
class MessageCenterPage extends StatelessWidget {
|
||||
const MessageCenterPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'消息中心',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)),
|
||||
body: GetBuilder<MessageCenterLogic>(
|
||||
init: MessageCenterLogic(),
|
||||
builder: (logic) {
|
||||
return Column(
|
||||
children: [
|
||||
TabBar(
|
||||
isScrollable: true,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
),
|
||||
indicator: BoxDecoration(),
|
||||
tabAlignment: TabAlignment.start,
|
||||
controller: logic.tabController,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 0),
|
||||
tabs: logic.titleArr
|
||||
.map((it) => Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: it == logic.titleArr.first
|
||||
? Colors.transparent
|
||||
: Color(0x1AFFFFFF),
|
||||
width: 1)),
|
||||
),
|
||||
child: Text(it),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: logic.tabController,
|
||||
children: logic.titleArr
|
||||
.asMap()
|
||||
.map((key, value) => MapEntry(
|
||||
key,
|
||||
MessageCenterSubPage(MessageType.values[key])
|
||||
.keepAlive))
|
||||
.values
|
||||
.toList()),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import 'message_center_sub_page.dart';
|
||||
import 'widget/message_items.dart';
|
||||
|
||||
abstract class MessageCenterSubLogic extends GetxController {
|
||||
final dataSource = [];
|
||||
int page = 1;
|
||||
bool isLoading = true;
|
||||
|
||||
RefreshController? refreshController;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
fetchPageData();
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
fetchPageData({bool isRefresh = true, bool showLoading = false}) {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
if (showLoading) {
|
||||
isLoading = true;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Widget instanceChildItem(int index);
|
||||
}
|
||||
|
||||
//评论
|
||||
class MessageCommentLogic extends MessageCenterSubLogic {
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return MessageCommentItem(dataSource[index]);
|
||||
}
|
||||
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true, bool showLoading = false}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh, showLoading: showLoading);
|
||||
final res = await MineService.getDynamicList(pageNumber: page, msgType: 2);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
//点赞
|
||||
class MessageThumbUpLogic extends MessageCenterSubLogic {
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true, bool showLoading = false}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh, showLoading: showLoading);
|
||||
final res = await MineService.getDynamicList(pageNumber: page, msgType: 1);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return MessageThumbItem(dataSource[index]);
|
||||
}
|
||||
}
|
||||
|
||||
MessageCenterSubLogic instanceController(MessageType type) {
|
||||
switch (type) {
|
||||
case MessageType.comment:
|
||||
return MessageCommentLogic();
|
||||
case MessageType.thumb:
|
||||
return MessageThumbUpLogic();
|
||||
default:
|
||||
throw '未知的类型${type}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
import 'message_center_sub_controller.dart';
|
||||
|
||||
enum MessageType {
|
||||
comment(1), //评论
|
||||
thumb(2); //点赞
|
||||
|
||||
final int type;
|
||||
const MessageType(this.type);
|
||||
}
|
||||
|
||||
class MessageCenterSubPage extends StatefulWidget {
|
||||
final MessageType type;
|
||||
const MessageCenterSubPage(this.type, {super.key});
|
||||
|
||||
@override
|
||||
State<MessageCenterSubPage> createState() => _MessageCenterSubPageState();
|
||||
}
|
||||
|
||||
class _MessageCenterSubPageState extends State<MessageCenterSubPage>
|
||||
with RouteAware {
|
||||
late MessageCenterSubLogic logic;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
logic = instanceController(widget.type);
|
||||
}
|
||||
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
super.didPopNext();
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
super.dispose();
|
||||
routeObserver.unsubscribe(this);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MessageCenterSubLogic>(
|
||||
init: logic,
|
||||
global: false,
|
||||
builder: (controller) => pullYsRefresh(
|
||||
onLoading: (_) => controller.fetchPageData(isRefresh: false),
|
||||
onRefresh: (_) => controller.fetchPageData(),
|
||||
onInit: (_) => controller.refreshController = _,
|
||||
child: () {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
if (controller.dataSource.isEmpty) return CErrorWidget();
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 12),
|
||||
itemCount: controller.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return controller.instanceChildItem(index);
|
||||
},
|
||||
);
|
||||
}()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
class UnReadMsgNumModel {
|
||||
UnReadMsgNumModel({
|
||||
this.dtCount,
|
||||
this.incomeCount,
|
||||
this.msgCount,
|
||||
});
|
||||
|
||||
UnReadMsgNumModel.fromJson(dynamic json) {
|
||||
dtCount = json['dtCount'];
|
||||
incomeCount = json['incomeCount'];
|
||||
msgCount = json['msgCount'];
|
||||
}
|
||||
num? dtCount;
|
||||
num? incomeCount;
|
||||
num? msgCount;
|
||||
UnReadMsgNumModel copyWith({
|
||||
num? dtCount,
|
||||
num? incomeCount,
|
||||
num? msgCount,
|
||||
}) =>
|
||||
UnReadMsgNumModel(
|
||||
dtCount: dtCount ?? this.dtCount,
|
||||
incomeCount: incomeCount ?? this.incomeCount,
|
||||
msgCount: msgCount ?? this.msgCount,
|
||||
);
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['dtCount'] = dtCount;
|
||||
map['incomeCount'] = incomeCount;
|
||||
map['msgCount'] = msgCount;
|
||||
return map;
|
||||
}
|
||||
|
||||
num get allNum => (dtCount ?? 0) + (incomeCount ?? 0) + (msgCount ?? 0);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/message/message_dynamic_list.dart';
|
||||
import 'package:hgdj/hj_page/user_center_page/user_center_page.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../community/post_detail_page/post_detail_page.dart';
|
||||
|
||||
/// 点赞
|
||||
class MessageThumbItem extends StatelessWidget {
|
||||
final MessageDynamicList model;
|
||||
const MessageThumbItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Get.to(() => PostDetailPage(
|
||||
argument: PostDetailPageArgument(id: model.objId, models: []))),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"#$lTypeText",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
model.createdAt.utcToYMD(),
|
||||
style: TextStyle(
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
7.sizeBoxH,
|
||||
Text(
|
||||
'${model.objName}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0x8CFFFFFF),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${model.likeCount}人为你点赞",
|
||||
style: TextStyle(
|
||||
color: Color(0x59FFFFFF),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String get lTypeText {
|
||||
// video:对帖子点赞 comment:对评论点赞
|
||||
switch (model.msgType) {
|
||||
case 'like_msg':
|
||||
return '帖子点赞';
|
||||
case 'like_comment_msg':
|
||||
return '评论点赞';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MessageCommentItem extends StatelessWidget {
|
||||
final MessageDynamicList model;
|
||||
|
||||
const MessageCommentItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(() => PostDetailPage(
|
||||
argument: PostDetailPageArgument(id: model.objId, models: []))),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(() => UserCenterPage(uid: model.sendUid ?? 0)),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: model.sendAvatar ?? '',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
model.sendName ?? '',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFFFFFFF),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
DateTimeUtil.utc2iso(model.createdAt),
|
||||
style: TextStyle(
|
||||
color: Color(0xFF989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
11.sizeBoxH,
|
||||
Text(
|
||||
model.content ?? '',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 82,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: model.objCover ?? '',
|
||||
height: 60,
|
||||
width: 60,
|
||||
borderRadius: 6,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/mine/official_list_item_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
|
||||
class MineDriveLogic extends GetxController {
|
||||
/// null 表示未加载(loading 态),非 null 表示已加载(可能为空)
|
||||
List<OfficialListItemModel>? allList;
|
||||
List<OfficialListItemModel> groupList = [];
|
||||
List<OfficialListItemModel> officialList = [];
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
Future<void> loadData() async {
|
||||
final list = await MineService.getOfficialLists() ?? [];
|
||||
allList = list;
|
||||
// position == 2 归商务合作,其余归官方社群
|
||||
officialList = list.where((e) => e.position == 2).toList();
|
||||
groupList = list.where((e) => e.position != 2).toList();
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/text_util.dart';
|
||||
|
||||
import '../../../hj_model/mine/official_list_item_model.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/loading/loading_center_widget.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'mine_drive_logic.dart';
|
||||
|
||||
class MineDrivePage extends StatelessWidget {
|
||||
const MineDrivePage({super.key});
|
||||
|
||||
static const _titleStyle =
|
||||
TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600);
|
||||
static const _subtitleStyle =
|
||||
TextStyle(color: Color(0xff525252), fontSize: 12);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('加群开车')),
|
||||
body: GetBuilder<MineDriveLogic>(
|
||||
init: MineDriveLogic(),
|
||||
builder: (controller) {
|
||||
if (controller.allList == null) return const LoadingCenterWidget();
|
||||
if (controller.allList!.isEmpty) {
|
||||
return CErrorWidget(retryOnTap: controller.loadData);
|
||||
}
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (controller.groupList.isNotEmpty)
|
||||
_buildSection(
|
||||
title: '官方社群',
|
||||
subtitle: '一起看片一起分享心得',
|
||||
list: controller.groupList),
|
||||
if (controller.officialList.isNotEmpty)
|
||||
_buildSection(
|
||||
title: '商务合作',
|
||||
subtitle: '代理合作/商务合作',
|
||||
list: controller.officialList),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 一个分组区块:标题 + 副标题 + 列表卡片
|
||||
Widget _buildSection({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required List<OfficialListItemModel> list,
|
||||
}) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
18.sizeBoxH,
|
||||
Text(title, style: _titleStyle),
|
||||
6.sizeBoxH,
|
||||
Text(subtitle, style: _subtitleStyle),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Column(children: list.map(_buildItem).toList()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 单条社群/合作项
|
||||
Widget _buildItem(OfficialListItemModel item) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => launchUrlToWeb(item.officialUrl ?? ''),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||
height: 80,
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: item.officialImg ?? '', width: 40, height: 40),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item.officialName ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 14, height: 1.1),
|
||||
),
|
||||
if (TextUtil.isNotEmpty(item.officialDesc)) ...[
|
||||
6.sizeBoxH,
|
||||
Text(
|
||||
item.officialDesc ?? '',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff525252), fontSize: 12, height: 1.1),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: 26,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: const Text(
|
||||
'立即加入',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/swipe_action_item.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../hj_model/mine/follow_user_list_model.dart';
|
||||
import 'mine_follow_sub_page.dart';
|
||||
import 'widget/follow_list_items.dart';
|
||||
|
||||
abstract class MineFollowSubLogic extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
final int? uid;
|
||||
MineFollowSubLogic(this.uid);
|
||||
|
||||
RxInt loadCount = 0.obs;
|
||||
|
||||
final dataSource = [];
|
||||
|
||||
RefreshController? refreshController;
|
||||
int pageNumber = 1;
|
||||
bool isLoading = true;
|
||||
bool isGridStyle = false;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
fetchPageData();
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
fetchPageData({bool isRefresh = true}) {
|
||||
if (isRefresh) {
|
||||
pageNumber = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Widget instanceChildItem(int index);
|
||||
}
|
||||
|
||||
class ActressController extends MineFollowSubLogic {
|
||||
ActressController(super.uid);
|
||||
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final retModel = await MineService.fetchCollectList<FollowUserModel>(
|
||||
'actress',
|
||||
page: pageNumber,
|
||||
size: 10,
|
||||
uid: uid,
|
||||
);
|
||||
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
retModel?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(retModel?.list ?? []);
|
||||
pageNumber += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return FollowActressItem(dataSource[index]);
|
||||
}
|
||||
}
|
||||
|
||||
class BloggerController extends MineFollowSubLogic {
|
||||
BloggerController(super.uid);
|
||||
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final retModel = await CommonService.getFollowUsers(
|
||||
pageNumber: pageNumber,
|
||||
pageSize: 10,
|
||||
uid: uid,
|
||||
);
|
||||
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
retModel?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(retModel?.list ?? []);
|
||||
pageNumber += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
final model = dataSource[index] as FollowUserModel;
|
||||
// 用 ObjectKey 而不是 ValueKey(uid):uid 是可空的,两条 uid 都为 null 就是重复 key,
|
||||
// Flutter 会直接抛 Duplicate keys 白屏。绑对象身份天然唯一,且能让展开状态不串项
|
||||
return SwipeActionItem(
|
||||
key: ObjectKey(model),
|
||||
actionText: '取消关注',
|
||||
onAction: () => _unfollow(model),
|
||||
child: FollowBloggerItem(model),
|
||||
);
|
||||
}
|
||||
|
||||
/// 正在取关的项:按钮收起动画那 200ms 里还能再点一次,防重复请求
|
||||
final _unfollowing = <FollowUserModel>{};
|
||||
|
||||
/// 左滑取关:成功才从列表摘掉(接口内部会 emit CollectStatusModel 同步其他页面)
|
||||
Future<void> _unfollow(FollowUserModel model) async {
|
||||
if (!_unfollowing.add(model)) return;
|
||||
try {
|
||||
final ok = await MineService.getFollow(model.uid, false);
|
||||
if (!ok) return showToast('取消关注失败,请重试');
|
||||
dataSource.remove(model); // 按对象移除,不用 index——异步期间列表可能已刷新
|
||||
update();
|
||||
} finally {
|
||||
_unfollowing.remove(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TopicController extends MineFollowSubLogic {
|
||||
TopicController(super.uid);
|
||||
@override
|
||||
bool get isGridStyle => true;
|
||||
|
||||
@override
|
||||
fetchPageData({bool isRefresh = true}) async {
|
||||
super.fetchPageData(isRefresh: isRefresh);
|
||||
final retModel = await MineService.fetchCollectList<TagsBean>("tag",
|
||||
page: pageNumber, size: 10, uid: uid);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
retModel?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(retModel?.list ?? []);
|
||||
pageNumber += 1;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget instanceChildItem(int index) {
|
||||
return FollowTopicItem(dataSource[index]);
|
||||
}
|
||||
}
|
||||
|
||||
MineFollowSubLogic instanceController(FollowType type, {int? uid}) {
|
||||
switch (type) {
|
||||
case FollowType.user:
|
||||
return BloggerController(uid);
|
||||
case FollowType.topic:
|
||||
return TopicController(uid);
|
||||
case FollowType.actress:
|
||||
return ActressController(uid);
|
||||
default:
|
||||
throw 'type 没有定义';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../tools_base/loading/loading_center_widget.dart';
|
||||
import '../../../tools_base/refresh/pull_refresh.dart';
|
||||
import '../../../tools_base/widget/swipe_action_item.dart';
|
||||
import 'mine_follow_sub_logic.dart';
|
||||
|
||||
enum FollowType {
|
||||
user,
|
||||
topic,
|
||||
actress;
|
||||
}
|
||||
|
||||
class MineFollowSubPage extends StatelessWidget {
|
||||
final FollowType pageType; // 0 博主, 1 粉丝, 2 话题
|
||||
final int? uid;
|
||||
|
||||
MineFollowSubPage({super.key, required this.pageType, this.uid});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// 用默认 global:global:false 下 widget 销毁不会 Get.delete,onClose 永远不触发,
|
||||
// 往 logic 里加 Timer/订阅就会静默泄漏。本页单实例(入口只在「我的」,链路不成环)故不加 tag;
|
||||
// 将来若做成多 pageType 并存的 TabBarView,得按 pageType 补 tag,否则三个 tab 抢同一个实例
|
||||
body: GetBuilder<MineFollowSubLogic>(
|
||||
init: instanceController(pageType, uid: uid),
|
||||
builder: (logic) {
|
||||
return pullYsRefresh(
|
||||
onRefresh: (refreshController) => logic.fetchPageData(),
|
||||
onLoading: (refreshController) => logic.fetchPageData(isRefresh: false),
|
||||
onInit: (ctr) => logic.refreshController = ctr,
|
||||
child: () {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.dataSource.isEmpty)
|
||||
return CErrorWidget(
|
||||
retryOnTap: () {
|
||||
logic.fetchPageData();
|
||||
},
|
||||
);
|
||||
if (logic.isGridStyle)
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 12),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 5,
|
||||
childAspectRatio: 165 / 143,
|
||||
),
|
||||
itemCount: logic.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return logic.instanceChildItem(index);
|
||||
},
|
||||
);
|
||||
// 一滚动就收起左滑展开的那项,免得滑走了还留着个张开的
|
||||
return NotificationListener<ScrollStartNotification>(
|
||||
onNotification: (_) {
|
||||
SwipeActionItem.closeOpened();
|
||||
return false;
|
||||
},
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
itemCount: logic.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return logic.instanceChildItem(index);
|
||||
},
|
||||
),
|
||||
);
|
||||
}());
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'mine_follow_sub_page.dart';
|
||||
|
||||
class MineFollowingPage extends StatelessWidget {
|
||||
const MineFollowingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('我的关注')),
|
||||
body: MineFollowSubPage(pageType: FollowType.user),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/mine/follow_user_list_model.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../community/community_tag_page/community_tag_page.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
/// 关注列表 - 女优项
|
||||
class FollowActressItem extends StatefulWidget {
|
||||
final FollowUserModel model;
|
||||
const FollowActressItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
State<FollowActressItem> createState() => _FollowActressItemState();
|
||||
}
|
||||
|
||||
class _FollowActressItemState extends State<FollowActressItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
// type:1 原跳女优主页 ActressMainPage(已下线),pushToPersonCenter 内 type==1 已 return null
|
||||
// → 当前点击不跳转,待产品确认替代页(objcId 与用户中心 uid 非同一体系,不能直接跳 UserCenterPage)
|
||||
pushToPersonCenter(widget.model.objcId, type: 1);
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 13.w, vertical: 16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff242424),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: widget.model.portrait ?? "",
|
||||
width: 43,
|
||||
height: 43,
|
||||
borderRadius: 6,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.model.name ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"粉丝: ${widget.model.fans?.countStr}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xff666666),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final isFollow = widget.model.hasFollow ?? false;
|
||||
final res = await MineService.postCollect(
|
||||
widget.model.objcId, 'actresss', !isFollow);
|
||||
if (res) {
|
||||
widget.model.hasFollow = !isFollow;
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 11.w, vertical: 5.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .3),
|
||||
borderRadius: BorderRadius.circular(40)),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.model.hasFollow == true) ...[
|
||||
Image.asset('collect_red.png'.commonImgPath, width: 16.w),
|
||||
7.w.sizeBoxW,
|
||||
],
|
||||
Text(
|
||||
widget.model.hasFollow == true ? '已关注' : '关注',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 关注列表 - 博主项
|
||||
class FollowBloggerItem extends StatefulWidget {
|
||||
final FollowUserModel model;
|
||||
const FollowBloggerItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
State<FollowBloggerItem> createState() => _FollowBloggerItemState();
|
||||
}
|
||||
|
||||
class _FollowBloggerItemState extends State<FollowBloggerItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
pushToPersonCenter(widget.model.uid ?? 0);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(bottom: 18, left: 16, right: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: widget.model.portrait ?? "",
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
widget.model.name ?? "",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${widget.model.totalWorks ?? 0} 作品',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x8CFFFFFF),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${widget.model.fans?.countStr} 粉丝',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x8CFFFFFF),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Image.asset(
|
||||
'arrow_right_grey.webp'.commonImgPath,
|
||||
color: Color(0xffDCDCDC),
|
||||
width: 24,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 关注列表 - 话题项
|
||||
class FollowTopicItem extends StatefulWidget {
|
||||
final TagsBean model;
|
||||
const FollowTopicItem(this.model, {super.key});
|
||||
|
||||
@override
|
||||
State<FollowTopicItem> createState() => _FollowTopicItemState();
|
||||
}
|
||||
|
||||
class _FollowTopicItemState extends State<FollowTopicItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
Get.to(() => CommunityTagDetailPage(model: widget.model));
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: widget.model.coverImg ?? "",
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
borderRadius: 8,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"#${widget.model.name}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${widget.model.videoCount.countStr}个帖子',
|
||||
style: TextStyle(color: Color(0xffcccccc), fontSize: 10),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final isCollected = widget.model.hasCollected ?? false;
|
||||
final res = await MineService.postCollect(
|
||||
widget.model.id, "tag", !isCollected);
|
||||
if (res) {
|
||||
widget.model.hasCollected = !isCollected;
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 108,
|
||||
height: 27,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF68804),
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.model.hasCollected == true ? '已关注' : '关注',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
class PointItem {
|
||||
double x;
|
||||
double y;
|
||||
int? index;
|
||||
bool isSelected;
|
||||
bool isError;
|
||||
bool isFirstSelected;
|
||||
double angle;
|
||||
|
||||
PointItem({
|
||||
required this.x,
|
||||
required this.y,
|
||||
this.index,
|
||||
this.isSelected = false,
|
||||
this.isError = false,
|
||||
this.isFirstSelected = false,
|
||||
this.angle = double.infinity,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || other is PointItem && runtimeType == other.runtimeType && x == other.x && y == other.y;
|
||||
|
||||
@override
|
||||
int get hashCode => x.hashCode ^ y.hashCode;
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../../tools_base/debug_log.dart';
|
||||
import '../model/point_item.dart';
|
||||
import 'line_painter.dart';
|
||||
|
||||
/// 当点被选中时的回调函数
|
||||
typedef OnHitPoint = void Function(List<int> result);
|
||||
|
||||
/// 手势滑动结束时的回调函数
|
||||
/// [result] 已经选择的所有点的结果集
|
||||
typedef OnComplete = void Function(List<int> result);
|
||||
|
||||
class GesturePasswordWidget extends StatefulWidget with DiagnosticableTreeMixin {
|
||||
/// GesturePasswordWidget 的 width 和 height.
|
||||
final double size;
|
||||
|
||||
///用来判断点是否被选中的区域大小,值越大识别越精准.
|
||||
final double identifySize;
|
||||
|
||||
///正常情况下展示的widget
|
||||
final Widget? normalItem;
|
||||
|
||||
///选中情况下展示的widget
|
||||
final Widget? selectedItem;
|
||||
|
||||
/// 错误情况下展示的widget,只有设置了[minLength]或[answer]时才会生效,
|
||||
/// 1)当[minLength]不为null时,如果选择的点的数量小于minLength,则展示[errorItem],
|
||||
/// 比如设置了 minLength = 4,但是选择的点的结果集为 [0,1,3],共选择了3个点,小于4;
|
||||
/// 2)当[answer]不为null时,如果选择的点的结果集和[answer]不相等,则展示[errorItem],
|
||||
/// 比如 answer = [0,1,2,4,7],但是选择的点的结果集为[0,1,2,5,8],和answer不相等;
|
||||
/// 另外,[errorItem]的展示时长由[completeWaitMilliseconds]控制。
|
||||
final Widget? errorItem;
|
||||
|
||||
/// 当这个点被选中时要展示的widget,其展示时长由[hitShowMilliseconds]控制,达到展示时长
|
||||
/// 后继续展示[selectedItem]。
|
||||
final Widget? hitItem;
|
||||
|
||||
///正常情况下显示的箭头控件。
|
||||
///跟随手势旋转时,x轴正方向为0度,所以如果你使用了箭头,确保箭头指向x轴正方向。
|
||||
final Widget? arrowItem;
|
||||
|
||||
///错误情况下显示的箭头控件,如果设置了[errorArrowItem],则必须设置[arrowItem],
|
||||
///否则[errorArrowItem]不会展示。
|
||||
///跟随手势旋转时,x轴正方向为0度,所以如果你使用了箭头,确保箭头指向x轴正方向。
|
||||
final Widget? errorArrowItem;
|
||||
|
||||
///[arrowItem]和[errorArrowItem]在x轴上的偏移,原点在[normalItem]的中心。
|
||||
///当 -1 < [arrowXAlign] < 1 时,[arrowItem]和[errorArrowItem]在[normalItem]范围内进行绘制;
|
||||
///当[arrowXAlign] > 1 或者[arrowXAlign] < -1时,在[normalItem]范围外进行绘制;
|
||||
final double arrowXAlign;
|
||||
|
||||
///[arrowItem]和[errorArrowItem]在y轴上的偏移,原点在[normalItem]的中心。
|
||||
///当 -1 < [arrowYAlign] < 1 时,[arrowItem]和[errorArrowItem]在[normalItem]范围内进行绘制;
|
||||
///当[arrowYAlign] > 1 或者[arrowYAlign] < -1时,在[normalItem]范围外进行绘制;
|
||||
final double arrowYAlign;
|
||||
|
||||
///单行个数,总个数等于 singleLineCount * singleLineCount.
|
||||
final int singleLineCount;
|
||||
|
||||
///GesturePasswordWidget的背景色,默认为 [Theme.of].[scaffoldBackgroundColor]
|
||||
final Color? color;
|
||||
|
||||
///当点被选中时的回调函数
|
||||
final OnHitPoint? onHitPoint;
|
||||
|
||||
///手势滑动结束时的回调函数
|
||||
final OnComplete? onComplete;
|
||||
|
||||
///线的颜色
|
||||
final Color lineColor;
|
||||
|
||||
///错误场景下线的颜色,见[errorItem]
|
||||
final Color errorLineColor;
|
||||
|
||||
///线的宽度
|
||||
final double lineWidth;
|
||||
|
||||
/// 是否采用宽松策略,默认为true。
|
||||
/// 考虑这种情况:第一个点选中了 index = 0 的点,第二个点选中了 index = 6的点,
|
||||
/// 此时index = 0,index = 3,index = 6这三个点在一条直线上,
|
||||
/// 如果loose为true,输出为[0,3,6],
|
||||
/// 如果loose为false,输出为[0,6].
|
||||
final bool loose;
|
||||
|
||||
///正确的结果,demo: [0, 1, 2, 4, 7]
|
||||
final List<int>? answer;
|
||||
|
||||
///最后选择的所有点及绘制的直线在屏幕上展示的时间,时间结束后,清除所有点,恢复到初始状态,
|
||||
///时间结束之前 GesturePasswordWidget 不再接受任何手势事件。
|
||||
final int completeWaitMilliseconds;
|
||||
|
||||
/// 只是用来展示用,不能触摸
|
||||
final bool ignoring;
|
||||
|
||||
///见[hitItem]
|
||||
final int hitShowMilliseconds;
|
||||
|
||||
///如果设置了此值,则长度不够时显示[errorItem]和[errorLineColor].
|
||||
final int? minLength;
|
||||
|
||||
///是否开启触觉反馈(命中点轻震、绘制错误较重震),默认开启
|
||||
final bool enableHaptic;
|
||||
|
||||
GesturePasswordWidget({
|
||||
super.key,
|
||||
this.size = 300.0,
|
||||
this.identifySize = 50.0,
|
||||
this.normalItem,
|
||||
this.selectedItem,
|
||||
this.errorItem,
|
||||
this.hitItem,
|
||||
this.arrowItem,
|
||||
this.errorArrowItem,
|
||||
this.arrowXAlign = 0.6,
|
||||
this.arrowYAlign = 0.0,
|
||||
this.singleLineCount = 3,
|
||||
this.color,
|
||||
this.ignoring = false,
|
||||
this.onHitPoint,
|
||||
this.onComplete,
|
||||
this.lineColor = Colors.green,
|
||||
this.errorLineColor = Colors.redAccent,
|
||||
this.lineWidth = 2.0,
|
||||
this.answer,
|
||||
this.loose = true,
|
||||
this.completeWaitMilliseconds = 300,
|
||||
this.hitShowMilliseconds = 40,
|
||||
this.minLength,
|
||||
this.enableHaptic = true,
|
||||
}) : assert(singleLineCount > 1, 'singLineCount must not be smaller than 1'),
|
||||
assert(identifySize > 0),
|
||||
assert(size > identifySize),
|
||||
assert(!(errorArrowItem != null && arrowItem == null), 'when arrowItem == null, errorArrowItem will not be shown.');
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('size', size));
|
||||
properties.add(DoubleProperty('identifySize', identifySize));
|
||||
properties.add(DiagnosticsProperty<Widget>('normalItem', normalItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('selectedItem', selectedItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('errorItem', errorItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('hitItem', hitItem));
|
||||
properties.add(DiagnosticsProperty<Widget>('arrowItem', arrowItem));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Widget>('errorArrowItem', errorArrowItem),
|
||||
);
|
||||
properties.add(DoubleProperty('arrowXAlign', arrowXAlign));
|
||||
properties.add(DoubleProperty('arrowYAlign', arrowYAlign));
|
||||
properties.add(IntProperty('singleLineCount', singleLineCount));
|
||||
properties.add(ColorProperty('color', color));
|
||||
properties.add(DiagnosticsProperty<OnHitPoint>('onHitPoint', onHitPoint));
|
||||
properties.add(DiagnosticsProperty<OnComplete>('onComplete', onComplete));
|
||||
properties.add(ColorProperty('lineColor', lineColor));
|
||||
properties.add(ColorProperty('errorLineColor', errorLineColor));
|
||||
properties.add(IterableProperty('answer', answer));
|
||||
properties.add(DoubleProperty('lineWidth', lineWidth));
|
||||
properties.add(FlagProperty(
|
||||
'loose',
|
||||
value: loose,
|
||||
ifFalse: 'loose: false',
|
||||
ifTrue: 'loose: true',
|
||||
defaultValue: true,
|
||||
));
|
||||
properties.add(
|
||||
IntProperty('completeWaitMilliseconds', completeWaitMilliseconds),
|
||||
);
|
||||
properties.add(IntProperty('hitShowMilliseconds', hitShowMilliseconds));
|
||||
properties.add(IntProperty('minLength', minLength));
|
||||
}
|
||||
|
||||
@override
|
||||
State<GesturePasswordWidget> createState() => _GesturePasswordWidgetState();
|
||||
}
|
||||
|
||||
class _GesturePasswordWidgetState extends State<GesturePasswordWidget> {
|
||||
late Point origin;
|
||||
late int totalCount;
|
||||
Point<double>? lastPoint;
|
||||
Widget? normalItem;
|
||||
Widget? defaultNormalItem;
|
||||
Widget? selectedItem;
|
||||
Widget? defaultSelectedItem;
|
||||
Widget? errorItem;
|
||||
Widget? defaultErrorItem;
|
||||
Color? lineColor;
|
||||
|
||||
String tipText = '绘制解锁图形';
|
||||
bool _completing = false; // 完成动画期间锁定手势,避免动画未结束就开始下一笔
|
||||
|
||||
final points = <PointItem>[];
|
||||
final linePoints = <Point<double>>[];
|
||||
final result = <int>[];
|
||||
final double defaultSize = 10.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
defaultNormalItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.greenAccent,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
defaultSelectedItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
defaultErrorItem = Container(
|
||||
width: defaultSize,
|
||||
height: defaultSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.answer?.isNotEmpty ?? false) {
|
||||
tipText = '验证解锁图形';
|
||||
}
|
||||
|
||||
lineColor = widget.lineColor;
|
||||
normalItem = widget.normalItem ?? defaultNormalItem;
|
||||
selectedItem = widget.selectedItem ?? defaultSelectedItem;
|
||||
errorItem = widget.errorItem ?? defaultErrorItem;
|
||||
|
||||
totalCount = widget.singleLineCount * widget.singleLineCount;
|
||||
origin = Point<double>(widget.size * 0.5, widget.size * 0.5);
|
||||
calculatePointPosition();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(GesturePasswordWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.lineColor != oldWidget.lineColor) {
|
||||
lineColor = widget.lineColor;
|
||||
}
|
||||
|
||||
if (widget.normalItem != oldWidget.normalItem) {
|
||||
normalItem = widget.normalItem ?? defaultNormalItem;
|
||||
}
|
||||
|
||||
if (widget.selectedItem != oldWidget.selectedItem) {
|
||||
selectedItem = widget.selectedItem ?? defaultSelectedItem;
|
||||
}
|
||||
|
||||
if (widget.errorItem != oldWidget.errorItem) {
|
||||
errorItem = widget.errorItem ?? defaultErrorItem;
|
||||
}
|
||||
|
||||
if (widget.singleLineCount != oldWidget.singleLineCount ||
|
||||
widget.size != oldWidget.size ||
|
||||
widget.identifySize != oldWidget.identifySize) {
|
||||
totalCount = widget.singleLineCount * widget.singleLineCount;
|
||||
origin = Point<double>(widget.size * 0.5, widget.size * 0.5);
|
||||
points.clear();
|
||||
calculatePointPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//预览小点的边长:每行 singleLineCount 个 10px 的点 + 之间 8px 间距。必须按 singleLineCount 算,
|
||||
//写死值(原来是 56)比内容宽时 Wrap 默认贴左上,整块预览会偏左偏上,且换行数只对 3x3 成立
|
||||
final previewSize = 10.0 * widget.singleLineCount + 8 * (widget.singleLineCount - 1);
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: previewSize,
|
||||
height: previewSize,
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: points
|
||||
.map((e) => Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
//选中态靠加粗边框填实心,边框画在 10x10 内部,不影响尺寸和对齐
|
||||
border: Border.all(width: e.isSelected ? 5 : 1.0, color: const Color(0xff6792ff)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text(
|
||||
tipText,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: widget.ignoring || _completing,
|
||||
child: Container(
|
||||
color: widget.color ?? Theme.of(context).scaffoldBackgroundColor,
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: Stack(
|
||||
children: createPointsWidget()
|
||||
..add(
|
||||
GestureDetector(
|
||||
onPanDown: handlePanDown,
|
||||
onPanUpdate: handlePanUpdate,
|
||||
onPanEnd: handlePanEnd,
|
||||
onPanCancel: () {
|
||||
handlePanEnd(null);
|
||||
},
|
||||
child: CustomPaint(
|
||||
painter: LinePainter(
|
||||
points: linePoints,
|
||||
lineColor: lineColor,
|
||||
lineWidth: widget.lineWidth,
|
||||
),
|
||||
willChange: true,
|
||||
size: Size(widget.size, widget.size),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//计算每个点的位置
|
||||
void calculatePointPosition() {
|
||||
double initX = widget.identifySize * 0.5;
|
||||
double initY = widget.identifySize * 0.5;
|
||||
double gap = (widget.size - widget.identifySize) / (widget.singleLineCount - 1);
|
||||
|
||||
for (int i = 0; i < totalCount; i++) {
|
||||
double centerX = initX + i % widget.singleLineCount * gap;
|
||||
double centerY = initY + i ~/ widget.singleLineCount * gap;
|
||||
var point = PointItem(x: centerX, y: centerY, index: i);
|
||||
//展示模式(ignoring)下预置 answer 命中的点为选中
|
||||
if (widget.ignoring && (widget.answer?.contains(i) ?? false)) {
|
||||
point.isSelected = true;
|
||||
}
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
//创建每个点的widget
|
||||
List<Widget> createPointsWidget() {
|
||||
return points.map<Widget>((p) {
|
||||
double reference = 1 - (widget.identifySize / widget.size);
|
||||
double x = (p.x - origin.x) / (widget.size * 0.5) / reference;
|
||||
double y = (p.y - origin.y) / (widget.size * 0.5) / reference;
|
||||
|
||||
Widget? child = normalItem;
|
||||
if (p.isError) {
|
||||
child = errorItem;
|
||||
} else if (p.isFirstSelected) {
|
||||
child = widget.hitItem;
|
||||
} else if (p.isSelected) {
|
||||
child = selectedItem;
|
||||
}
|
||||
|
||||
Widget? arrowItem = widget.arrowItem;
|
||||
if (p.isError && widget.errorArrowItem != null) {
|
||||
arrowItem = widget.errorArrowItem;
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment(x, y),
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
width: widget.identifySize,
|
||||
height: widget.identifySize,
|
||||
alignment: Alignment.center,
|
||||
child: widget.arrowItem == null || p.angle == double.infinity
|
||||
? child
|
||||
: Transform.rotate(
|
||||
angle: p.angle,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
child!,
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
widget.arrowXAlign,
|
||||
widget.arrowYAlign,
|
||||
),
|
||||
child: arrowItem,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void handlePanDown(DragDownDetails details) {
|
||||
Point<double> curPoint = Point(details.localPosition.dx, details.localPosition.dy);
|
||||
final point = calculateHitPoint(curPoint);
|
||||
if (point != null) {
|
||||
if (!linePoints.contains(Point(point.x, point.y))) {
|
||||
addPointToResult(point.index);
|
||||
setState(() {
|
||||
point.isSelected = true;
|
||||
linePoints.add(Point(point.x, point.y));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handlePanUpdate(DragUpdateDetails details) {
|
||||
Point<double> curPoint = Point(details.localPosition.dx, details.localPosition.dy);
|
||||
final hitPoint = calculateHitPoint(curPoint);
|
||||
if (hitPoint != null) {
|
||||
if (!linePoints.contains(Point(hitPoint.x, hitPoint.y))) {
|
||||
final drawPoint = Point(hitPoint.x, hitPoint.y);
|
||||
//宽松策略下,若三点共线则自动将中间的点设置为选中状态。
|
||||
if (widget.loose && linePoints.isNotEmpty) {
|
||||
handleLooseCase(points[result.last], hitPoint);
|
||||
}
|
||||
|
||||
//处理箭头的角度展示
|
||||
if (widget.arrowItem != null) {
|
||||
for (int i = 0; i < result.length - 1; i++) {
|
||||
final p1 = Point(points[result[i]].x, points[result[i]].y);
|
||||
final p2 = Point(points[result[i + 1]].x, points[result[i + 1]].y);
|
||||
points[result[i]].angle = calculateAngle(p1, p2);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty) {
|
||||
final p1 = Point(points[result.last].x, points[result.last].y);
|
||||
points[result.last].angle = calculateAngle(p1, Point(hitPoint.x, hitPoint.y));
|
||||
}
|
||||
addPointToResult(hitPoint.index);
|
||||
setState(() {
|
||||
linePoints.remove(lastPoint);
|
||||
hitPoint.isSelected = true;
|
||||
linePoints.add(drawPoint);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (linePoints.isNotEmpty) {
|
||||
if (widget.arrowItem != null) {
|
||||
final p1 = Point(points[result.last].x, points[result.last].y);
|
||||
points[result.last].angle = calculateAngle(p1, curPoint);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
linePoints.remove(lastPoint);
|
||||
linePoints.add(curPoint);
|
||||
});
|
||||
lastPoint = curPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handlePanEnd(DragEndDetails? details) async {
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
linePoints.removeLast();
|
||||
|
||||
if ((widget.answer != null && widget.answer!.join() != result.join()) ||
|
||||
(widget.minLength != null && widget.minLength! > result.length)) {
|
||||
lineColor = widget.errorLineColor;
|
||||
if (widget.minLength != null && widget.minLength! > result.length) {
|
||||
tipText = '请至少链接4个点';
|
||||
} else {
|
||||
tipText = '图形绘制不正确';
|
||||
}
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
points[result[i]].isError = true;
|
||||
}
|
||||
if (widget.enableHaptic) HapticFeedback.mediumImpact(); //绘制错误:较重震动
|
||||
}
|
||||
|
||||
//清除最后一个点的角度
|
||||
points[result.last].angle = double.infinity;
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_completing = true;
|
||||
});
|
||||
await Future.delayed(Duration(
|
||||
milliseconds: widget.completeWaitMilliseconds,
|
||||
));
|
||||
_completing = false;
|
||||
lineColor = widget.lineColor;
|
||||
widget.onComplete?.call(result);
|
||||
debugLog('handlePanEnd answer=${widget.answer}, result = $result');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
for (final p in points) {
|
||||
p.isSelected = false;
|
||||
p.isError = false;
|
||||
p.angle = double.infinity;
|
||||
}
|
||||
linePoints.clear();
|
||||
result.clear();
|
||||
});
|
||||
}
|
||||
|
||||
//计算命中的点
|
||||
PointItem? calculateHitPoint(Point<double> curPoint) {
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
final p = Point(points[i].x, points[i].y);
|
||||
if (p.distanceTo(curPoint) < widget.identifySize * 0.5) {
|
||||
if (points[i].isSelected) {
|
||||
return null;
|
||||
}
|
||||
return points[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void addPointToResult(int? index) {
|
||||
if (widget.enableHaptic) HapticFeedback.selectionClick(); //每命中一个点轻震,提升手感
|
||||
result.add(index ?? 0);
|
||||
widget.onHitPoint?.call(result);
|
||||
|
||||
if (widget.hitItem != null) {
|
||||
setState(() {
|
||||
points[index!].isFirstSelected = true;
|
||||
});
|
||||
Future.delayed(Duration(milliseconds: widget.hitShowMilliseconds), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
points[index!].isFirstSelected = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//根据海伦公式计算三角形面积,面积为0时视为三点共线。如果这个点还在共线的中间,
|
||||
//则将其设置为选中状态,并将其添加到result中。
|
||||
void handleLooseCase(PointItem pre, PointItem next) {
|
||||
List<int?> midItems = [];
|
||||
for (final item in points) {
|
||||
if (item != pre && item != next && item.isSelected == false) {
|
||||
final itemDrawPoint = Point<double>(item.x, item.y);
|
||||
final preDrawPoint = Point<double>(pre.x, pre.y);
|
||||
final nextDrawPoint = Point<double>(next.x, next.y);
|
||||
double a = itemDrawPoint.distanceTo(preDrawPoint);
|
||||
double b = itemDrawPoint.distanceTo(nextDrawPoint);
|
||||
double c = preDrawPoint.distanceTo(nextDrawPoint);
|
||||
double p = (a + b + c) * 0.5;
|
||||
double area = p * (p - a) * (p - b) * (p - c);
|
||||
|
||||
double halfDistance = c * 0.5;
|
||||
Point<double> mid = Point(
|
||||
(pre.x + next.x) * 0.5,
|
||||
(pre.y + next.y) * 0.5,
|
||||
);
|
||||
|
||||
if (area - 0.5 <= 0 && itemDrawPoint.distanceTo(mid) < halfDistance) {
|
||||
item.isSelected = true;
|
||||
midItems.add(item.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next.index! > pre.index!) {
|
||||
midItems.sort((a, b) => a! - b!);
|
||||
} else {
|
||||
midItems.sort((a, b) => b! - a!);
|
||||
}
|
||||
|
||||
for (final index in midItems) {
|
||||
addPointToResult(index);
|
||||
}
|
||||
}
|
||||
|
||||
//计算两点之间连线和 x 轴的夹角,返回弧度
|
||||
double calculateAngle(Point p1, Point p2) {
|
||||
return atan2((p2.y - p1.y), (p2.x - p1.x)); //弧度
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class LinePainter extends CustomPainter {
|
||||
final List<Point>? points;
|
||||
final Color? lineColor;
|
||||
final double? lineWidth;
|
||||
|
||||
LinePainter({
|
||||
this.points,
|
||||
this.lineColor,
|
||||
this.lineWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
if (points != null && points!.length > 1) {
|
||||
Paint paint = Paint();
|
||||
paint.strokeWidth = lineWidth!;
|
||||
paint.color = lineColor!;
|
||||
paint.isAntiAlias = true;
|
||||
paint.style = PaintingStyle.fill;
|
||||
paint.strokeCap = StrokeCap.round;
|
||||
|
||||
for (int i = 0; i < points!.length - 1; i++) {
|
||||
canvas.drawLine(
|
||||
Offset(points![i].x as double, points![i].y as double),
|
||||
Offset(points![i + 1].x as double, points![i + 1].y as double),
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(LinePainter oldDelegate) => true;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'mine_publish_sub_logic.dart';
|
||||
|
||||
class MinePublishLogic extends GetxController {
|
||||
MinePublishLogic();
|
||||
|
||||
List<VideoModel> checkedList = [];
|
||||
|
||||
List<VideoModel> dataSource = [];
|
||||
|
||||
void addChecked(VideoModel videoModel) {
|
||||
checkedList.add(videoModel);
|
||||
update();
|
||||
}
|
||||
|
||||
void removeChecked(VideoModel videoModel) {
|
||||
checkedList.remove(videoModel);
|
||||
update();
|
||||
}
|
||||
|
||||
void clearChecked() {
|
||||
checkedList.clear();
|
||||
update();
|
||||
}
|
||||
|
||||
bool get isAllChecked {
|
||||
if (dataSource.isEmpty || checkedList.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return checkedList.length == dataSource.length;
|
||||
}
|
||||
|
||||
bool get isEmptyChecked {
|
||||
return checkedList.isEmpty;
|
||||
}
|
||||
|
||||
void selectAllOrNot() {
|
||||
if (isAllChecked) {
|
||||
checkedList.clear();
|
||||
} else {
|
||||
checkedList.clear();
|
||||
checkedList.addAll(dataSource);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void delete() async {
|
||||
if (checkedList.isEmpty) {
|
||||
showToast('请选择要删除的项');
|
||||
return;
|
||||
}
|
||||
String resultMsg = await MineService.deletePublishes(
|
||||
ids: checkedList.map((e) => e.id ?? '').toList());
|
||||
if (resultMsg.isEmpty) {
|
||||
checkedList.clear();
|
||||
update();
|
||||
Get.find<MinePublishSubLogic>(tag: '2').loadData(isRefresh: true);
|
||||
showToast('删除成功');
|
||||
} else {
|
||||
showToast(resultMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/user/wallet_model.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../home/widget/publist_entry_alert.dart';
|
||||
import '../make_money/mine_withdrawal_record_page.dart';
|
||||
import '../make_money/withdrawal_page.dart';
|
||||
import 'mine_publish_main_logic.dart';
|
||||
import 'mine_publish_sub_logic.dart';
|
||||
import 'mine_publish_sub_page.dart';
|
||||
|
||||
///我发布的帖子
|
||||
class MinePublishPostPage extends StatefulWidget {
|
||||
const MinePublishPostPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _MinePublishPostPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MinePublishPostPageState extends State<MinePublishPostPage>
|
||||
with TickerProviderStateMixin {
|
||||
final tabs = ['已发布', '待审核', '未通过'];
|
||||
WalletModel? get wallet => globalStore.wallet;
|
||||
late final tabCtr = TabController(length: tabs.length, vsync: this);
|
||||
|
||||
bool inEdit = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
tabCtr.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabCtr.removeListener(_onTabChanged);
|
||||
tabCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _setSelectedTabDatas() {
|
||||
final logic = Get.find<MinePublishLogic>();
|
||||
if (tabCtr.index == 2) {
|
||||
logic.dataSource = List<VideoModel>.from(
|
||||
Get.find<MinePublishSubLogic>(tag: '2').dataList ?? []);
|
||||
} else {
|
||||
logic.dataSource = [];
|
||||
}
|
||||
logic.update();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder(
|
||||
init: MinePublishLogic(),
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("创作中心",
|
||||
style: textStyle(18, Color(0xE5FFFFFF), FontWeight.w600)),
|
||||
actions: [
|
||||
Visibility(
|
||||
visible: tabCtr.index == 2,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
if (inEdit) {
|
||||
controller.clearChecked();
|
||||
} else {
|
||||
_setSelectedTabDatas();
|
||||
}
|
||||
setState(() => inEdit = !inEdit);
|
||||
},
|
||||
child: Text(
|
||||
inEdit ? '完成' : '编辑',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Consumer<GlobalStore>(
|
||||
builder: (context, store, child) {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 18),
|
||||
padding: EdgeInsets.fromLTRB(0, 14, 0, 22),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildItem(
|
||||
'收益余额',
|
||||
'${wallet?.income}',
|
||||
'立即提现',
|
||||
() => Get.to(WithdrawalPage()),
|
||||
),
|
||||
_buildItem(
|
||||
'累计收益',
|
||||
'${wallet?.vidIncome}',
|
||||
'业绩明细',
|
||||
() =>
|
||||
Get.to(RecordsPage(RecordType.income)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildTabbar(),
|
||||
Expanded(child: _buildContent())
|
||||
],
|
||||
),
|
||||
if (!inEdit)
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: 50,
|
||||
child: PublishButton(
|
||||
entry: PublishEntry.community,
|
||||
),
|
||||
),
|
||||
if (inEdit)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 62,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF303030),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
controller.selectAllOrNot();
|
||||
},
|
||||
child: Container(
|
||||
width: 112,
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
controller.isAllChecked ? '取消全选' : '全选',
|
||||
style: TextStyle(
|
||||
color: Color(0x99FFFFFF), fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 20,
|
||||
color: Color(0x1AFFFFFF),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: controller.isEmptyChecked
|
||||
? null
|
||||
: () {
|
||||
controller.delete();
|
||||
},
|
||||
child: Container(
|
||||
width: 112,
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFFF52C56).withValues(
|
||||
alpha: controller.isEmptyChecked ? 0.5 : 1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'删除(${controller.checkedList.length})',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(
|
||||
alpha: controller.isEmptyChecked
|
||||
? 0.5
|
||||
: 1),
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_buildTabbar() {
|
||||
return TabBar(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
tabAlignment: TabAlignment.fill,
|
||||
controller: tabCtr,
|
||||
onTap: (index) {
|
||||
if (index != 2 && inEdit) {
|
||||
// 如果切换到非"未通过"标签页,强制退出编辑模式
|
||||
setState(() {
|
||||
inEdit = false;
|
||||
Get.find<MinePublishLogic>().clearChecked();
|
||||
});
|
||||
}
|
||||
},
|
||||
tabs: List.generate(
|
||||
tabs.length,
|
||||
(index) => Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 4),
|
||||
child: FittedBox(
|
||||
child: Text(tabs[index]),
|
||||
),
|
||||
)),
|
||||
isScrollable: false,
|
||||
labelColor: Colors.white.withValues(alpha: .9),
|
||||
labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
unselectedLabelColor: Colors.white.withValues(alpha: 0.35),
|
||||
unselectedLabelStyle:
|
||||
const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
|
||||
indicator: CustomIndicator(
|
||||
width: 13,
|
||||
height: 3,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
isGradient: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildItem(
|
||||
String title, String count, String actionTitle, Function() action) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .7),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Text(
|
||||
count,
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontWeight: FontWeight.w500, fontSize: 24),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
GestureDetector(
|
||||
onTap: () => action.call(),
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 25,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(13.5)),
|
||||
color: Color(0xffF68804),
|
||||
),
|
||||
child: Text(
|
||||
actionTitle,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// TabBarView 内容(外层调用处 line 134 已用 Expanded 包裹,这里不再套 Expanded,否则双 Expanded 冲突)
|
||||
Widget _buildContent() {
|
||||
return TabBarView(
|
||||
controller: tabCtr,
|
||||
physics: inEdit && tabCtr.index == 2
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: [
|
||||
MinePublishSubPage(status: 1, inEdit: false).keepAlive,
|
||||
MinePublishSubPage(status: 0, inEdit: false).keepAlive,
|
||||
MinePublishSubPage(status: 2, inEdit: inEdit).keepAlive,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
|
||||
class MinePublishSubLogic extends ListBaseLogic<VideoModel> {
|
||||
final int status;
|
||||
|
||||
MinePublishSubLogic({required this.status});
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData({bool isRefresh = true}) =>
|
||||
fetchData(isRefresh: isRefresh, fetch: _fetch);
|
||||
|
||||
Future<(List<VideoModel>?, bool)> _fetch(int page) async {
|
||||
final res = await MineService.fetchPublishes(page: page, status: status);
|
||||
return (res.list, res.hasNext ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../community/widget/community_post_widget.dart';
|
||||
import 'mine_publish_main_logic.dart';
|
||||
import 'mine_publish_sub_logic.dart';
|
||||
|
||||
typedef OnItemCheckCallback = void Function(
|
||||
VideoModel videoModel, bool isChecked);
|
||||
|
||||
class MinePublishSubPage extends StatelessWidget {
|
||||
final int status;
|
||||
final bool inEdit;
|
||||
final OnItemCheckCallback? onItemCheck;
|
||||
const MinePublishSubPage(
|
||||
{super.key, this.status = 0, this.inEdit = false, this.onItemCheck});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logic = Get.find<MinePublishLogic>();
|
||||
|
||||
return GetBuilder<MinePublishSubLogic>(
|
||||
init: MinePublishSubLogic(status: status),
|
||||
tag: status.toString(),
|
||||
builder: (controller) {
|
||||
return pullYsRefresh(
|
||||
onInit: (refr) => controller.refreshCtr = refr,
|
||||
onRefresh: (refr) => controller.loadData(),
|
||||
onLoading: (refr) => controller.loadData(isRefresh: false),
|
||||
child: () {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
if (controller.isEmptyData) return CErrorWidget();
|
||||
final list = controller.dataList!;
|
||||
return ListView.builder(
|
||||
itemCount: list.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CommunityPostWidget(
|
||||
videoModel: list[index],
|
||||
isMyPublish: true,
|
||||
showCheck: inEdit,
|
||||
isChecked: logic.checkedList.contains(list[index]),
|
||||
onItemCheck: (videoModel, isChecked) {
|
||||
if (isChecked) {
|
||||
logic.addChecked(videoModel);
|
||||
} else {
|
||||
logic.removeChecked(videoModel);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
import 'dart:convert' as convert;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_alert.dart';
|
||||
import '../../../../hj_utils/light_model.dart';
|
||||
import '../../../../hj_utils/store_keys.dart';
|
||||
import '../../../../tools_base/refresh/pull_refresh.dart';
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../tools_base/loading/loading_alert_widget.dart';
|
||||
import '../../../tools_base/loading/loading_center_widget.dart';
|
||||
import 'model/alipay_bank_list_model.dart';
|
||||
import 'widget/add_bank_dialog.dart';
|
||||
|
||||
///银行卡管理
|
||||
class BankCardHomePage extends StatefulWidget {
|
||||
final AccountInfoModel? selectModel;
|
||||
BankCardHomePage({super.key, this.selectModel});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _BankCardHomePageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _BankCardHomePageState extends State<BankCardHomePage> {
|
||||
bool isEdit = false;
|
||||
|
||||
bool loading = true;
|
||||
|
||||
List<AccountInfoModel> aliList = [];
|
||||
|
||||
AccountInfoModel? selectItem;
|
||||
List<AccountInfoModel> deleteList = [];
|
||||
RefreshController? refreshCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getAliListData();
|
||||
}
|
||||
|
||||
void _onDeleteAccount() async {
|
||||
if (deleteList.isEmpty) return;
|
||||
var isDelete = await CommonAlert.show(content: "是否确认删除账号");
|
||||
if (!isDelete) return;
|
||||
LoadingAlertWidget.show();
|
||||
bool result = false;
|
||||
for (var model in deleteList) {
|
||||
result = await MineService.bankCardDelete(id: model.id);
|
||||
if (result) {
|
||||
var result = await lightKV.getString(StoreKeys.LAST_A_ACCOUNT);
|
||||
if (result != null) {
|
||||
AccountInfoModel listBean =
|
||||
AccountInfoModel.fromMap(convert.jsonDecode(result));
|
||||
if (model.id == listBean.id) {
|
||||
lightKV.setString(StoreKeys.LAST_A_ACCOUNT, "");
|
||||
}
|
||||
}
|
||||
aliList.remove(model);
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
if (result) {
|
||||
showToast("删除成功");
|
||||
isEdit = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
LoadingAlertWidget.cancel();
|
||||
}
|
||||
|
||||
void _getAliListData() async {
|
||||
final model = await MineService.getBankCards();
|
||||
aliList.clear();
|
||||
aliList.addAll(model?.list ?? []);
|
||||
refreshCtr?.refreshCompleted();
|
||||
if (aliList.isNotEmpty == true) {
|
||||
if (widget.selectModel != null) {
|
||||
selectItem = widget.selectModel;
|
||||
} else {
|
||||
// selectItem = aliList.firstOrNull;
|
||||
}
|
||||
}
|
||||
loading = false;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: Text('选择银行卡',
|
||||
style: textStyle(18, Color(0xE5FFFFFF), FontWeight.w600)),
|
||||
leading: BackButton(
|
||||
onPressed: () => Get.back(result: selectItem),
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
isEdit = !isEdit;
|
||||
if (isEdit) {
|
||||
deleteList.clear();
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Text(
|
||||
isEdit ? "完成" : "编辑",
|
||||
style: TextStyle(
|
||||
color: isEdit ? AppColors.actionRed : Color(0x8CFFFFFF),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
18.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: () {
|
||||
if (loading) return LoadingCenterWidget();
|
||||
if (aliList.isEmpty == true) return CErrorWidget();
|
||||
return Container(
|
||||
margin: EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => refreshCtr = ctr,
|
||||
enablePullUp: false,
|
||||
onRefresh: (ctr) => _getAliListData(),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 80),
|
||||
shrinkWrap: true,
|
||||
itemCount: aliList.length ?? 0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _buildCardView(index, aliList[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}()),
|
||||
_buildCardAddView(),
|
||||
],
|
||||
),
|
||||
if (isEdit)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () => _onDeleteAccount(),
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 35, vertical: 20),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF52C56),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"删除",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardView(int index, AccountInfoModel? item) {
|
||||
bool isSelected = false;
|
||||
if (isEdit) {
|
||||
if (deleteList.contains(item) == true) {
|
||||
isSelected = true;
|
||||
}
|
||||
} else {
|
||||
isSelected = selectItem?.id == item?.id;
|
||||
}
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
if (isEdit) {
|
||||
if (deleteList.contains(item) == true) {
|
||||
deleteList.remove(item);
|
||||
} else {
|
||||
deleteList.add(item!);
|
||||
}
|
||||
} else {
|
||||
Get.back(result: item);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(bottom: 12),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(9)),
|
||||
color: const Color(0x0DFFFFFF)),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
item?.getBankName() ?? "",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xE5ffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
9.sizeBoxW,
|
||||
Text(
|
||||
item?.actName ?? "",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xE5ffffff),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Text("卡号:${item?.act}",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0))
|
||||
],
|
||||
),
|
||||
),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isSelected
|
||||
? CupertinoIcons.checkmark_circle_fill
|
||||
: CupertinoIcons.circle,
|
||||
color: isSelected ? Color(0xffE1351F) : Color(0xff999999),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//添加银行卡
|
||||
_buildCardAddView() {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () async {
|
||||
final res = await Get.dialog(AddBankDialog(),
|
||||
barrierColor: Colors.black.withValues(alpha: .7));
|
||||
if (res != null) {
|
||||
_getAliListData();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 35, vertical: 20),
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(3)),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"添加银行卡",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'bankcard_info.dart';
|
||||
|
||||
class ApBankListModel {
|
||||
List<AccountInfoModel>? list;
|
||||
|
||||
static ApBankListModel fromJson(Map<String, dynamic>? map) {
|
||||
ApBankListModel apBankListModel = ApBankListModel();
|
||||
if (map == null) return apBankListModel;
|
||||
if (map['list'] is List) {
|
||||
apBankListModel.list = (map['list'] as List).map((o) => AccountInfoModel.fromMap(o)).toList();
|
||||
}
|
||||
return apBankListModel;
|
||||
}
|
||||
}
|
||||
|
||||
class AccountInfoModel {
|
||||
String? id;
|
||||
String? actName;
|
||||
String? act;
|
||||
String? bankCode;
|
||||
String? cardType;
|
||||
|
||||
static AccountInfoModel fromMap(Map<String, dynamic>? map) {
|
||||
AccountInfoModel listBean = AccountInfoModel();
|
||||
if (map == null) return AccountInfoModel();
|
||||
listBean.id = map['id'];
|
||||
listBean.actName = map['actName'];
|
||||
listBean.act = map['act'];
|
||||
listBean.bankCode = map['bankCode'];
|
||||
listBean.cardType = map['cardType'];
|
||||
return listBean;
|
||||
}
|
||||
|
||||
///获取银行名称
|
||||
String? getBankName() {
|
||||
if (bankCode?.isEmpty == true) {
|
||||
return "";
|
||||
}
|
||||
BankCardModel? model = BankcardInfo().getBankInfoMap(bankCode!);
|
||||
if (model == null) {
|
||||
return BankcardInfo().getBankName(bankCode!);
|
||||
}
|
||||
return model.bankName ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
class BankCardModel {
|
||||
String? bankLogoId;
|
||||
String? bankName;
|
||||
String? bankCardId;
|
||||
|
||||
BankCardModel(this.bankLogoId, this.bankName, this.bankCardId);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
|
||||
|
||||
import 'alipay_bank_list_model.dart';
|
||||
|
||||
class BankcardInfo {
|
||||
|
||||
|
||||
static final BankcardInfo _instance = BankcardInfo._internal();
|
||||
|
||||
factory BankcardInfo() => _instance;
|
||||
|
||||
BankcardInfo._internal();
|
||||
|
||||
var bankName = ['招商银行', '中国银行', '农业银行', '交通银行', '建设银行', '民生银行', '中信银行', '光大银行', '华夏银行', '工商银行', '兴业银行', '国家开发银行'];
|
||||
var bankCardId = ['CMB', 'BOC', 'ABC', 'COMM', 'CCB', 'CMBC', 'CITIC', 'CEB', 'HXBANK', 'ICBC', 'CIB', 'CDB'];
|
||||
|
||||
BankCardModel? getBankInfoMap(String bankId) {
|
||||
Map<String, dynamic> map = Map();
|
||||
for (int i = 0; i < bankCardId.length; i++) {
|
||||
BankCardModel model = new BankCardModel(null, bankName[i], bankCardId[i]);
|
||||
map[bankCardId[i]] = model;
|
||||
}
|
||||
bool contains = map.containsKey(bankId);
|
||||
if (contains) {
|
||||
return map[bankId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
///支付宝支持银行
|
||||
String? getBankName(String bankId) {
|
||||
Map<String, String> map = Map();
|
||||
map["SRCB"] = "深圳农村商业银行";
|
||||
map["BGB"] = "广西北部湾银行";
|
||||
map["SHRCB"] = "上海农村商业银行";
|
||||
map["BJBANK"] = "北京银行";
|
||||
map["WHCCB"] = "威海市商业银行";
|
||||
map["BOZK"] = "周口银行";
|
||||
map["KORLABANK"] = "库尔勒市商业银行";
|
||||
map["SPABANK"] = "平安银行";
|
||||
map["SDEB"] = "顺德农商银行";
|
||||
map["HURCB"] = "湖北省农村信用社";
|
||||
map["WRCB"] = "无锡农村商业银行";
|
||||
map["BOCY"] = "朝阳银行";
|
||||
map["CZBANK"] = "浙商银行";
|
||||
map["HDBANK"] = "邯郸银行";
|
||||
map["BOC"] = "中国银行";
|
||||
map["BOD"] = "东莞银行";
|
||||
map["CCB"] = "中国建设银行";
|
||||
map["ZYCBANK"] = "遵义市商业银行";
|
||||
map["SXCB"] = "绍兴银行";
|
||||
map["GZRCU"] = "贵州省农村信用社";
|
||||
map["ZJKCCB"] = "张家口市商业银行";
|
||||
map["BOJZ"] = "锦州银行";
|
||||
map["BOP"] = "平顶山银行";
|
||||
map["HKB"] = "汉口银行";
|
||||
map["SPDB"] = "上海浦东发展银行";
|
||||
map["NXRCU"] = "宁夏黄河农村商业银行";
|
||||
map["NYNB"] = "广东南粤银行";
|
||||
map["GRCB"] = "广州农商银行";
|
||||
map["BOSZ"] = "苏州银行";
|
||||
map["HZCB"] = "杭州银行";
|
||||
map["HSBK"] = "衡水银行";
|
||||
map["HBC"] = "湖北银行";
|
||||
map["JXBANK"] = "嘉兴银行";
|
||||
map["HRXJB"] = "华融湘江银行";
|
||||
map["BODD"] = "丹东银行";
|
||||
map["AYCB"] = "安阳银行";
|
||||
map["EGBANK"] = "恒丰银行";
|
||||
map["CDB"] = "国家开发银行";
|
||||
map["TCRCB"] = "江苏太仓农村商业银行";
|
||||
map["NJCB"] = "南京银行";
|
||||
map["ZZBANK"] = "郑州银行";
|
||||
map["DYCB"] = "德阳商业银行";
|
||||
map["YBCCB"] = "宜宾市商业银行";
|
||||
map["SCRCU"] = "四川省农村信用";
|
||||
map["KLB"] = "昆仑银行";
|
||||
map["LSBANK"] = "莱商银行";
|
||||
map["YDRCB"] = "尧都农商行";
|
||||
map["CCQTGB"] = "重庆三峡银行";
|
||||
map["FDB"] = "富滇银行";
|
||||
map["JSRCU"] = "江苏省农村信用联合社";
|
||||
map["JNBANK"] = "济宁银行";
|
||||
map["CMB"] = "招商银行";
|
||||
map["JINCHB"] = "晋城银行JCBANK";
|
||||
map["FXCB"] = "阜新银行";
|
||||
map["WHRCB"] = "武汉农村商业银行";
|
||||
map["HBYCBANK"] = "湖北银行宜昌分行";
|
||||
map["TZCB"] = "台州银行";
|
||||
map["TACCB"] = "泰安市商业银行";
|
||||
map["XCYH"] = "许昌银行";
|
||||
map["CEB"] = "中国光大银行";
|
||||
map["NXBANK"] = "宁夏银行";
|
||||
map["HSBANK"] = "徽商银行";
|
||||
map["JJBANK"] = "九江银行";
|
||||
map["NHQS"] = "农信银清算中心";
|
||||
map["MTBANK"] = "浙江民泰商业银行";
|
||||
map["LANGFB"] = "廊坊银行";
|
||||
map["ASCB"] = "鞍山银行";
|
||||
map["KSRB"] = "昆山农村商业银行";
|
||||
map["YXCCB"] = "玉溪市商业银行";
|
||||
map["DLB"] = "大连银行";
|
||||
map["DRCBCL"] = "东莞农村商业银行";
|
||||
map["GCB"] = "广州银行";
|
||||
map["NBBANK"] = "宁波银行";
|
||||
map["BOYK"] = "营口银行";
|
||||
map["SXRCCU"] = "陕西信合";
|
||||
map["GLBANK"] = "桂林银行";
|
||||
map["BOQH"] = "青海银行";
|
||||
map["CDRCB"] = "成都农商银行";
|
||||
map["QDCCB"] = "青岛银行";
|
||||
map["HKBEA"] = "东亚银行";
|
||||
map["HBHSBANK"] = "湖北银行黄石分行";
|
||||
map["WZCB"] = "温州银行";
|
||||
map["TRCB"] = "天津农商银行";
|
||||
map["QLBANK"] = "齐鲁银行";
|
||||
map["GDRCC"] = "广东省农村信用社联合社";
|
||||
map["ZJTLCB"] = "浙江泰隆商业银行";
|
||||
map["GZB"] = "赣州银行";
|
||||
map["GYCB"] = "贵阳市商业银行";
|
||||
map["CQBANK"] = "重庆银行";
|
||||
map["DAQINGB"] = "龙江银行";
|
||||
map["CGNB"] = "南充市商业银行";
|
||||
map["SCCB"] = "三门峡银行";
|
||||
map["CSRCB"] = "常熟农村商业银行";
|
||||
map["SHBANK"] = "上海银行";
|
||||
map["JLBANK"] = "吉林银行";
|
||||
map["CZRCB"] = "常州农村信用联社";
|
||||
map["BANKWF"] = "潍坊银行";
|
||||
map["ZRCBANK"] = "张家港农村商业银行";
|
||||
map["FJHXBC"] = "福建海峡银行";
|
||||
map["ZJNX"] = "浙江省农村信用社联合社";
|
||||
map["LZYH"] = "兰州银行";
|
||||
map["JSB"] = "晋商银行";
|
||||
map["BOHAIB"] = "渤海银行";
|
||||
map["CZCB"] = "浙江稠州商业银行";
|
||||
map["YQCCB"] = "阳泉银行";
|
||||
map["SJBANK"] = "盛京银行";
|
||||
map["XABANK"] = "西安银行";
|
||||
map["BSB"] = "包商银行";
|
||||
map["JSBANK"] = "江苏银行";
|
||||
map["FSCB"] = "抚顺银行";
|
||||
map["HNRCU"] = "河南省农村信用";
|
||||
map["COMM"] = "交通银行";
|
||||
map["CITIC"] = "中信银行";
|
||||
map["XTB"] = "邢台银行";
|
||||
map["HXBANK"] = "华夏银行";
|
||||
map["HNRCC"] = "湖南省农村信用社";
|
||||
map["DYCCB"] = "东营市商业银行";
|
||||
map["ORBANK"] = "鄂尔多斯银行";
|
||||
map["BJRCB"] = "北京农村商业银行";
|
||||
map["XYBANK"] = "信阳银行";
|
||||
map["ZGCCB"] = "自贡市商业银行";
|
||||
map["CDCB"] = "成都银行";
|
||||
map["HANABANK"] = "韩亚银行";
|
||||
map["CMBC"] = "中国民生银行";
|
||||
map["LYBANK"] = "洛阳银行";
|
||||
map["GDB"] = "广东发展银行";
|
||||
map["ZBCB"] = "齐商银行";
|
||||
map["CBKF"] = "开封市商业银行";
|
||||
map["H3CB"] = "内蒙古银行";
|
||||
map["CIB"] = "兴业银行";
|
||||
map["CRCBANK"] = "重庆农村商业银行";
|
||||
map["SZSBK"] = "石嘴山银行";
|
||||
map["DZBANK"] = "德州银行";
|
||||
map["SRBANK"] = "上饶银行";
|
||||
map["LSCCB"] = "乐山市商业银行";
|
||||
map["JXRCU"] = "江西省农村信用";
|
||||
map["ICBC"] = "中国工商银行";
|
||||
map["JZBANK"] = "晋中市商业银行";
|
||||
map["HZCCB"] = "湖州市商业银行";
|
||||
map["NHB"] = "南海农村信用联社";
|
||||
map["XXBANK"] = "新乡银行";
|
||||
map["JRCB"] = "江苏江阴农村商业银行";
|
||||
map["YNRCC"] = "云南省农村信用社";
|
||||
map["ABC"] = "中国农业银行";
|
||||
map["GXRCU"] = "广西省农村信用";
|
||||
map["PSBC"] = "中国邮政储蓄银行";
|
||||
map["BZMD"] = "驻马店银行";
|
||||
map["ARCU"] = "安徽省农村信用社";
|
||||
map["GSRCU"] = "甘肃省农村信用";
|
||||
map["LYCB"] = "辽阳市商业银行";
|
||||
map["JLRCU"] = "吉林农信";
|
||||
map["URMQCCB"] = "乌鲁木齐市商业银行";
|
||||
map["XLBANK"] = "中山小榄村镇银行";
|
||||
map["CSCB"] = "长沙银行";
|
||||
map["JHBANK"] = "金华银行";
|
||||
map["BHB"] = "河北银行";
|
||||
map["NBYZ"] = "鄞州银行";
|
||||
map["LSBC"] = "临商银行";
|
||||
map["BOCD"] = "承德银行";
|
||||
map["SDRCU"] = "山东农信";
|
||||
map["NCB"] = "南昌银行";
|
||||
map["TCCB"] = "天津银行";
|
||||
map["WJRCB"] = "吴江农商银行";
|
||||
map["CBBQS"] = "城市商业银行资金清算中心";
|
||||
map["HBRCU"] = "河北省农村信用社";
|
||||
|
||||
return map[bankId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/config/address.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/net/http_manager.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_dialog.dart';
|
||||
|
||||
import '../../make_money/alipay_ccdcapi_model.dart';
|
||||
|
||||
class AddBankDialog extends StatefulWidget {
|
||||
const AddBankDialog({super.key});
|
||||
|
||||
@override
|
||||
State<AddBankDialog> createState() => _AddBankDialogState();
|
||||
}
|
||||
|
||||
class _AddBankDialogState extends State<AddBankDialog> {
|
||||
late final bankNameCtr = TextEditingController();
|
||||
late final bankNoCtr = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonDialog(
|
||||
canTapClose: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'添加银行卡',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Divider(height: .5, color: Colors.white.withValues(alpha: .1)),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
maxLength: 20,
|
||||
controller: bankNameCtr,
|
||||
style: TextStyle(color: Colors.white),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'[a-zA-Z\u4E00-\u9F5a]'))
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
hintText: '请输入姓名',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
maxLength: 20,
|
||||
controller: bankNoCtr,
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontWeight: FontWeight.w400),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[0-9]'))
|
||||
],
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入银行卡号',
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
24.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
if (bankNoCtr.text.isEmpty) {
|
||||
showToast('请输入银行卡号');
|
||||
return;
|
||||
}
|
||||
if (bankNameCtr.text.isEmpty) {
|
||||
showToast('请输入持卡人姓名');
|
||||
return;
|
||||
}
|
||||
_bankNumberVerifyReq();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
"立即提交",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
///校验银行卡信息
|
||||
void _bankNumberVerifyReq() async {
|
||||
String bankNum = bankNoCtr.text.trim();
|
||||
if (bankNum.isEmpty || bankNum.length < 13) {
|
||||
showToast("银行卡号错误");
|
||||
return;
|
||||
}
|
||||
LoadingHelper.showLoading();
|
||||
httpManager.fetchResponseByGET(Address.aliCcdApi + bankNum).then((ret) {
|
||||
LoadingHelper.dismissLoading();
|
||||
if (!ret.isSuccess) {
|
||||
showToast("银行卡验证失败.");
|
||||
return;
|
||||
}
|
||||
if (ret.isSuccess) {
|
||||
final model = ApcApiModel.fromMap(Map<String, dynamic>.from(ret.data));
|
||||
if (model?.validated ?? false) {
|
||||
// model.bank, model.cardType
|
||||
debugLog("model.bank-->", "${model?.bank}");
|
||||
debugLog("model.cardType-->", "${model?.cardType}");
|
||||
// _commonWithdrawReq(true, bank: model?.bank);
|
||||
_addBankCard(model!);
|
||||
} else {
|
||||
showToast("银行卡验证失败");
|
||||
}
|
||||
} else {
|
||||
showToast("银行卡验证失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_addBankCard(ApcApiModel model) async {
|
||||
final res = await MineService.addBankCard(
|
||||
model.key, bankNameCtr.text, model.bank, model.cardType);
|
||||
if (res) {
|
||||
Get.back(result: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import '../../../tools_base/event_bus/event_bus_util.dart';
|
||||
import '../../../tools_base/event_bus/events.dart';
|
||||
import 'bing_phone_page.dart';
|
||||
|
||||
class BindPhoneLogic extends GetxController {
|
||||
late final phoneCtr = TextEditingController();
|
||||
late final codeCtr = TextEditingController();
|
||||
late final countDownNof = ValueNotifier(-1);
|
||||
Timer? _timer;
|
||||
final PhonePageType pageType;
|
||||
|
||||
BindPhoneLogic(this.pageType);
|
||||
|
||||
@override
|
||||
onClose() {
|
||||
super.onClose();
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
onSendCode() async {
|
||||
if (_timer != null) return;
|
||||
if (phoneCtr.text.isEmpty || phoneCtr.text.length < 11) {
|
||||
showToast('请输入正确的手机号~');
|
||||
return;
|
||||
}
|
||||
final res = await MineService.postCaptchaCode(phoneCtr.text, pageType.type);
|
||||
if (res) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
countDownNof.value += 1;
|
||||
if (countDownNof.value == 60) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
countDownNof.value = -1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onBindPhone() async {
|
||||
if (phoneCtr.text.isEmpty) {
|
||||
showToast('请输入正确的手机号~');
|
||||
return;
|
||||
}
|
||||
if (codeCtr.text.isEmpty) {
|
||||
showToast('请输入验证码~');
|
||||
return;
|
||||
}
|
||||
if (pageType == PhonePageType.bind) {
|
||||
final res = await MineService.bindPhone(phoneCtr.text, codeCtr.text);
|
||||
if (res) {
|
||||
showToast('绑定成功');
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
}
|
||||
} else {
|
||||
final result =
|
||||
await globalStore.loginByMobile(phoneCtr.text, codeCtr.text);
|
||||
if (result != null) {
|
||||
showToast('登录成功');
|
||||
eventBus.emit(ReLoginEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import 'bind_phone_logic.dart';
|
||||
|
||||
enum PhonePageType {
|
||||
bind('绑定手机', 1, '立即绑定'),
|
||||
find('找回账号', 2, '立即找回');
|
||||
|
||||
final int type;
|
||||
final String title;
|
||||
final String buttonTitle;
|
||||
const PhonePageType(this.title, this.type, this.buttonTitle);
|
||||
}
|
||||
|
||||
class BindPhonePage extends StatelessWidget {
|
||||
final PhonePageType pageType;
|
||||
|
||||
const BindPhonePage({super.key, this.pageType = PhonePageType.bind});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<BindPhoneLogic>(
|
||||
init: BindPhoneLogic(pageType),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.pageType.title),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 47),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 42,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'手机号',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 16),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.phoneCtr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLength: 11,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入手机号码',
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
counterText: '',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 14)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Divider(
|
||||
height: .5,
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 42,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'验证码',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 16),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.codeCtr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLength: 6,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入手机验证码',
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
counterText: '',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 14)),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
GestureDetector(
|
||||
onTap: () => controller.onSendCode(),
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.countDownNof,
|
||||
builder:
|
||||
(BuildContext context, int value, Widget? child) {
|
||||
final iscountdown = value > -1;
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4, vertical: 4),
|
||||
child: Text(
|
||||
iscountdown ? '${60 - value}秒' : '获取验证码',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
fontSize: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Divider(
|
||||
height: .5,
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
GestureDetector(
|
||||
onTap: () => controller.onBindPhone(),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
controller.pageType.buttonTitle,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
/// 绑定邀请码逻辑
|
||||
class InviteBindLogic extends GetxController {
|
||||
// 邀请码输入框
|
||||
final codeCtr = TextEditingController();
|
||||
|
||||
/// 立即绑定
|
||||
void bind() async {
|
||||
if (codeCtr.text.isEmpty) {
|
||||
showToast('请输入邀请码~');
|
||||
return;
|
||||
}
|
||||
final res = await MineService.exchangeInviteCode(codeCtr.text);
|
||||
if (res) {
|
||||
showToast('绑定成功');
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
} else {
|
||||
showToast('绑定失败');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
codeCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import 'invite_bind_logic.dart';
|
||||
|
||||
/// 绑定邀请码页
|
||||
class InviteBindPage extends StatelessWidget {
|
||||
const InviteBindPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<InviteBindLogic>(
|
||||
init: InviteBindLogic(),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('绑定邀请码'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: 28, right: 28, top: 28),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Text(
|
||||
'输入邀请码',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'邀请码只能绑定一次 且不能修改',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
// 邀请码输入
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'邀请码',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 16),
|
||||
),
|
||||
24.sizeBoxW,
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.codeCtr,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
maxLength: 11,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '请输入邀请码(字母大写)',
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
counterText: '',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35),
|
||||
fontSize: 14)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Divider(
|
||||
height: .35,
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Text(
|
||||
'邀请1人 获得1天会员',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35), fontSize: 14),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
// 立即绑定按钮
|
||||
GestureDetector(
|
||||
onTap: controller.bind,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
'立即绑定',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/mine/exchange_record_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
class MineExchangeCodeLogic extends GetxController {
|
||||
final int type; //0-填写邀请码 1-填写兑换码
|
||||
TextEditingController controller = TextEditingController();
|
||||
List<ExchangeRecordModel> groupList = [];
|
||||
bool isLoadingHistoryData = true;
|
||||
RefreshController? refreshController; //由 pullYsRefresh 的 onInit 注入,组件负责释放
|
||||
int currentPage = 1;
|
||||
|
||||
MineExchangeCodeLogic({this.type = 0});
|
||||
|
||||
@override
|
||||
onReady() {
|
||||
super.onReady();
|
||||
if (type == 1) loadData();
|
||||
}
|
||||
|
||||
loadData({int page = 1}) async {
|
||||
try {
|
||||
final res = await MineService.getExchangeRecord(page, 20);
|
||||
if (res != null && res.data != null) {
|
||||
if (page == 1) groupList.clear();
|
||||
currentPage = page;
|
||||
groupList.addAll(res.data ?? []);
|
||||
}
|
||||
refreshController?.refreshCompleted();
|
||||
(res?.total ?? 0) > (res?.data?.length ?? 0)
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
} catch (e) {
|
||||
refreshController?.refreshCompleted();
|
||||
refreshController?.loadComplete();
|
||||
debugLog(e);
|
||||
}
|
||||
update();
|
||||
isLoadingHistoryData = false;
|
||||
}
|
||||
|
||||
loadMoreData() => loadData(page: currentPage + 1);
|
||||
|
||||
void onExChangeCode() async {
|
||||
if (controller.text.isEmpty) {
|
||||
showToast('请输入${type == 0 ? '邀请码' : '兑换码'}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == 1) {
|
||||
try {
|
||||
LoadingAlertWidget.show();
|
||||
bool ret = await MineService.postExchangeCode(controller.text);
|
||||
LoadingAlertWidget.cancel();
|
||||
if (ret == true) {
|
||||
controller.text = "";
|
||||
globalStore.updateUserInfo();
|
||||
showToast("兑换成功");
|
||||
loadData();
|
||||
}
|
||||
} catch (e) {
|
||||
LoadingAlertWidget.cancel();
|
||||
debugLog(e);
|
||||
}
|
||||
} else if (type == 0) {
|
||||
try {
|
||||
LoadingAlertWidget.show();
|
||||
bool ret = await MineService.getProxyBind(controller.text);
|
||||
LoadingAlertWidget.cancel();
|
||||
if (ret == true) {
|
||||
globalStore.meInfo?.inviterCode = controller.text;
|
||||
globalStore.updateUserInfo();
|
||||
showToast("绑定成功");
|
||||
Get.back(result: true);
|
||||
}
|
||||
} catch (e) {
|
||||
LoadingAlertWidget.cancel();
|
||||
debugLog(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/mine/exchange_record_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import 'mine_code_logic.dart';
|
||||
|
||||
//邀请码和兑换码
|
||||
class MineExchangeCodePage extends StatefulWidget {
|
||||
final int type; //0-填写邀请码 1-填写兑换码
|
||||
const MineExchangeCodePage({super.key, this.type = 0});
|
||||
|
||||
@override
|
||||
State<MineExchangeCodePage> createState() => _MineExchangeCodePageState();
|
||||
}
|
||||
|
||||
class _MineExchangeCodePageState extends State<MineExchangeCodePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(widget.type == 0 ? '邀请码' : '领取兑换')),
|
||||
body: GetBuilder<MineExchangeCodeLogic>(
|
||||
init: MineExchangeCodeLogic(type: widget.type),
|
||||
builder: (_) => Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 28, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (controller) => _.refreshController = controller,
|
||||
onRefresh: (controller) => _.loadData(),
|
||||
onLoading: (controller) => _.loadMoreData(),
|
||||
child: CustomScrollView(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: <Widget>[
|
||||
SliverToBoxAdapter(
|
||||
child: _buildContent(_),
|
||||
),
|
||||
if (widget.type == 1) ...[
|
||||
_buildHistoryTable(_),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => _.onExChangeCode(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"立即兑换",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryTable(MineExchangeCodeLogic logic) {
|
||||
if (logic.isLoadingHistoryData) {
|
||||
return SliverToBoxAdapter(child: LoadingCenterWidget());
|
||||
} else if (logic.groupList.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: CErrorWidget(errorMsg: "暂无兑换记录"),
|
||||
);
|
||||
} else {
|
||||
return SliverList.separated(
|
||||
itemBuilder: (context, index) {
|
||||
return _buildListItem(logic.groupList[index]);
|
||||
},
|
||||
itemCount: logic.groupList.length,
|
||||
separatorBuilder: (context, index) {
|
||||
return Divider(
|
||||
height: 1,
|
||||
color: Colors.black87.withValues(alpha: 0.1),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_buildContent(MineExchangeCodeLogic _) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"输入兑换码",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
"每个兑换码只能输入一次",
|
||||
style: TextStyle(
|
||||
color: Color(0xff525252),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
56.sizeBoxH,
|
||||
Container(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"兑换码",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
Expanded(
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.text,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
cursorColor: Colors.white,
|
||||
textAlign: TextAlign.left,
|
||||
controller: _.controller,
|
||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
||||
decoration: InputDecoration(
|
||||
hintText: '请输入${widget.type == 0 ? '邀请码' : '兑换码'}(字母大写)',
|
||||
hintStyle: TextStyle(color: Color(0xff434c55)),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
12.sizeBoxH,
|
||||
1.line,
|
||||
18.sizeBoxH,
|
||||
Text(
|
||||
"官方社群领取更多福利",
|
||||
style: TextStyle(
|
||||
color: Color(0xff525252),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
36.sizeBoxH,
|
||||
Text(
|
||||
"兑换记录",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildSectionItem('兑换码')),
|
||||
5.sizeBoxW,
|
||||
Expanded(child: _buildSectionItem('兑换类型')),
|
||||
5.sizeBoxW,
|
||||
Expanded(child: _buildSectionItem('兑换时间')),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildSectionItem(String title) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildListItem(ExchangeRecordModel item) {
|
||||
return Container(
|
||||
height: 44,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
item.code ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55), fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
item.desc ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55), fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${item.createdAt.utcToYMD(gap: '.')}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55), fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../hj_utils/permission_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import 'bing_phone_page.dart';
|
||||
import 'mine_scan_login_page.dart';
|
||||
|
||||
class MineFindAccountPage extends StatelessWidget {
|
||||
late final dataSource = [
|
||||
{
|
||||
'title': '手机号找回',
|
||||
'ontap': () =>
|
||||
Get.to(() => const BindPhonePage(pageType: PhonePageType.find)),
|
||||
},
|
||||
{
|
||||
'title': '凭证找回',
|
||||
'ontap': () async {
|
||||
if (await PermissionUtil.checkCameraPermission()) {
|
||||
Get.to(() => const MineScanLoginPage());
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'title': '联系客服',
|
||||
'ontap': () => pushToCustomService(),
|
||||
}
|
||||
];
|
||||
MineFindAccountPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(elevation: 0, title: Text('找回账号')),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.separated(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: dataSource.length,
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 0.5,
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final data = dataSource[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: data['ontap'] as Function()?,
|
||||
child: Column(
|
||||
children: [
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
(data['title'] ?? '').toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: 0.9)),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: Color(0xFFDCDCDC),
|
||||
)
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/splash/splash_page.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
/// 手势密码用途:设置 / 关闭 / 进 app 解锁校验
|
||||
enum MinePasswordType { setting, close, check }
|
||||
|
||||
class MinePasswordLogic extends GetxController {
|
||||
//本页是中间件 redirect 的目标(RouteSettings 只能带 arguments),没法改构造传参;
|
||||
//用 is 判断而不是隐式强转:路由栈里读到别的页的 arguments 时只降级不抛 TypeError
|
||||
final MinePasswordType type = _typeFromArgs();
|
||||
|
||||
static MinePasswordType _typeFromArgs() {
|
||||
final args = Get.arguments;
|
||||
return args is MinePasswordType ? args : MinePasswordType.setting;
|
||||
}
|
||||
|
||||
bool get isCheck => type == MinePasswordType.check;
|
||||
|
||||
String get title => isCheck ? '请输入解锁密码' : '请绘制锁屏图形';
|
||||
|
||||
/// 画完手势:设置/关闭成功即退出本页,解锁通过则回启动页重走流程
|
||||
Future<void> onComplete(List<int> result) async {
|
||||
if (result.length < 4) {
|
||||
showToast('请至少链接4个点');
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case MinePasswordType.setting:
|
||||
if (await globalStore.setLockPassword(result) == true) Get.back();
|
||||
case MinePasswordType.close:
|
||||
if (await globalStore.closeLockPassword(result) == true) Get.back();
|
||||
case MinePasswordType.check:
|
||||
if (await globalStore.checkLockPassword(result) == true)
|
||||
Get.toNamed(SplashPage.routeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../mine_gesture_pwd/widget/gesture_password_widget.dart';
|
||||
import 'mine_password_logic.dart';
|
||||
|
||||
/// 手势密码页:设置 / 关闭 / 解锁校验共用,用途由路由 arguments 传 [MinePasswordType]
|
||||
class MinePasswordPage extends StatelessWidget {
|
||||
static const routeName = '/MinePasswordPage';
|
||||
const MinePasswordPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MinePasswordLogic>(
|
||||
init: MinePasswordLogic(),
|
||||
//必须 false:type 来自路由 arguments,全局注册会让「关闭」复用上一次「设置/解锁」残留的 logic
|
||||
global: false,
|
||||
builder: (logic) => Scaffold(
|
||||
//解锁校验时还进不了 app,不给返回入口
|
||||
appBar: logic.isCheck ? null : AppBar(title: const Text('手势密码')),
|
||||
//必须给满宽约束:Column 横向是按最宽的子节点收缩的(这里 250),而 Scaffold 的 body
|
||||
//是贴 (0,0) 摆放的,不撑满就整块贴左边;撑满后 Column 默认的居中对齐才真正生效
|
||||
body: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
//没有 AppBar 顶着,用锁图标占位
|
||||
if (logic.isCheck) ...[
|
||||
65.sizeBoxH,
|
||||
Image.asset('mine_lock_password.png'.mineImgPath, width: 29),
|
||||
],
|
||||
55.sizeBoxH,
|
||||
Text(
|
||||
logic.title,
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: 0.9), fontSize: 18),
|
||||
),
|
||||
75.sizeBoxH,
|
||||
GesturePasswordWidget(
|
||||
lineColor: const Color(0xFFF52C56),
|
||||
errorLineColor: const Color(0xffDD001B),
|
||||
singleLineCount: 3,
|
||||
identifySize: 60.0,
|
||||
size: 250,
|
||||
minLength: 4,
|
||||
errorItem: Image.asset('error.webp'.mineImgPath,
|
||||
color: const Color(0xFFF52C56)),
|
||||
selectedItem: Image.asset('select.png'.mineImgPath,
|
||||
color: const Color(0xFFF52C56)),
|
||||
normalItem: Image.asset('normal.png'.mineImgPath),
|
||||
onComplete: logic.onComplete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_pickers/image_pickers.dart';
|
||||
import 'package:hgdj/hj_utils/image_util.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/event_bus_util.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/events.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/net/net_manager.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
import '../identity/mine_identity_page.dart';
|
||||
|
||||
class MineScanLginLogic extends GetxController {
|
||||
QRViewController? scanController;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scanController?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
onQRViewCreated(QRViewController controller) {
|
||||
scanController = controller;
|
||||
|
||||
scanController?.scannedDataStream.listen((scanData) {
|
||||
print('扫码结果:${scanData.code}');
|
||||
loginByQrValue(scanData.code ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
/// 开启本地相册
|
||||
openNativePhoto() async {
|
||||
// image_picker 走系统相册 intent,选图不需要存储权限,直接调起
|
||||
final images = await ImagePickers.pickerPaths(
|
||||
uiConfig: UIConfig(uiThemeColor: Colors.white),
|
||||
selectCount: 1,
|
||||
showCamera: false,
|
||||
cropConfig: CropConfig(enableCrop: false),
|
||||
);
|
||||
if (images.isNotEmpty) {
|
||||
final qrValue = await ImageUtil.decodeQr(images[0].path ?? '');
|
||||
if (qrValue == null || qrValue.isEmpty) {
|
||||
showToast('二维码错误~~');
|
||||
return;
|
||||
}
|
||||
loginByQrValue(qrValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否正在处理一次扫码登录。扫码流会按摄像头帧连续 emit,pauseCamera 异步拦不住已缓冲的帧,
|
||||
/// 不加锁会并发触发 N 次 loginByQr → token 被反复清空/轮换,服务端单会话把旧 token 全废,
|
||||
/// 满屏 5009/5005「用户信息已经过期」,最终 token 落空、换号失败。
|
||||
bool _isHandling = false;
|
||||
|
||||
/// 开始二维码登录
|
||||
loginByQrValue(String qrValue) async {
|
||||
if (_isHandling) return; // 一次扫码只处理一次,挡掉连续帧/重复触发
|
||||
if (qrValue.isEmpty) {
|
||||
showToast("二维码为空");
|
||||
return;
|
||||
}
|
||||
_isHandling = true;
|
||||
scanController?.pauseCamera();
|
||||
LoadingHelper.showLoading();
|
||||
var userInfo = await globalStore.loginByQr(qrValue);
|
||||
LoadingHelper.dismissLoading();
|
||||
// 必须拿到 token 才算成功:只有 uid 没 token 会留下空 token,下一个请求立刻被判过期
|
||||
if (userInfo?.uid == null || (userInfo?.token ?? '').isEmpty) {
|
||||
showToast("切换账号失败");
|
||||
scanController?.resumeCamera();
|
||||
_isHandling = false; // 失败放开,允许重试
|
||||
return;
|
||||
}
|
||||
// 刷新ua
|
||||
netManager.refreshUserAgent();
|
||||
showToast('登录成功');
|
||||
eventBus.emit(ReLoginEvent());
|
||||
}
|
||||
|
||||
/// 跳转凭证
|
||||
jumpToCertificate() => Get.to(MineAccountIdentityPage());
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
import 'mine_scan_login_logic.dart';
|
||||
|
||||
class MineScanLoginPage extends StatefulWidget {
|
||||
const MineScanLoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<MineScanLoginPage> createState() => _MineScanLoginPageState();
|
||||
}
|
||||
|
||||
class _MineScanLoginPageState extends State<MineScanLoginPage> {
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineScanLginLogic>(
|
||||
init: MineScanLginLogic(),
|
||||
builder: (controller) => Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.white),
|
||||
backgroundColor: Colors.transparent,
|
||||
title: Text(
|
||||
'扫码登录',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 18, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: controller.onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
overlayColor: Colors.black,
|
||||
borderColor: Colors.white,
|
||||
borderRadius: 0,
|
||||
borderLength: 20,
|
||||
borderWidth: 5,
|
||||
cutOutSize: 235),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.openNativePhoto,
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset('scan_photo.png'.mineImgPath, width: 48),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'相册',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: controller.jumpToCertificate,
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset('mine_id.png'.mineImgPath, width: 48),
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'我的凭证',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/version_util.dart';
|
||||
import 'package:hgdj/hj_utils/video_cache_manager.dart';
|
||||
import 'package:hgdj/tools_base/cache/cache_util.dart';
|
||||
import 'package:hgdj/tools_base/cache/image_cache_manager.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/image/image_data_handle/image_cache_disk.dart';
|
||||
import 'package:hgdj/tools_base/video_download/video_download_manager.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_alert.dart';
|
||||
|
||||
import '../../../alert/splash/update_dialog.dart';
|
||||
import '../identity/mine_identity_page.dart';
|
||||
import '../more_question/mine_qa_page.dart';
|
||||
import 'bing_phone_page.dart';
|
||||
import 'invite_bind_page.dart';
|
||||
import 'mine_find_account_page.dart';
|
||||
import 'mine_setting_profile_page.dart';
|
||||
import 'setting_avatar_page.dart';
|
||||
|
||||
class MineSettingPage extends StatefulWidget {
|
||||
const MineSettingPage({super.key});
|
||||
|
||||
@override
|
||||
State<MineSettingPage> createState() => _MineSettingPageState();
|
||||
}
|
||||
|
||||
class _MineSettingPageState extends State<MineSettingPage> {
|
||||
String cacheSize = ''; // 缓存大小
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getCacheSize();
|
||||
}
|
||||
|
||||
Future<void> getCacheSize() async {
|
||||
try {
|
||||
cacheSize = await loadCache();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final meInfo = context.watch<GlobalStore>().meInfo;
|
||||
final mobile = meInfo?.mobile ?? '';
|
||||
final inviterCode = meInfo?.inviterCode ?? '';
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('设置中心')),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
left: 10,
|
||||
top: 12,
|
||||
right: 10,
|
||||
),
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildItem(
|
||||
title: '头像',
|
||||
avatarUrl: meInfo?.portrait ?? '',
|
||||
onTap: () async {
|
||||
if (!globalStore.isVIP) {
|
||||
if (await CommonAlert.show(
|
||||
content: '您还不是VIP无法修改头像',
|
||||
subContent: '开通会员 即可解锁继续',
|
||||
showCancel: false,
|
||||
confirmText: '开通会员',
|
||||
)) {
|
||||
pushToWalletPage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Get.to(() => const SettingAvatarPage());
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '昵称',
|
||||
subTitle: meInfo?.name,
|
||||
onTap: () async {
|
||||
if (!globalStore.isVIP) {
|
||||
if (await CommonAlert.show(
|
||||
content: '您还不是VIP无法修改昵称!',
|
||||
subContent: '开通会员 即可解锁继续',
|
||||
showCancel: false,
|
||||
confirmText: '开通会员',
|
||||
)) {
|
||||
pushToWalletPage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Get.to(() =>
|
||||
const SettingProfilePage(SettingProfileType.nickName));
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '${Config.appName}ID',
|
||||
subTitle: meInfo?.uid.toString(),
|
||||
showCopy: true,
|
||||
onTap: () {}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '手机号码',
|
||||
subTitle: mobile.isEmpty ? '立即绑定' : mobile,
|
||||
onTap: () {
|
||||
Get.to(() => const BindPhonePage());
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '账号找回',
|
||||
onTap: () {
|
||||
Get.to(() => MineFindAccountPage());
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '邀请码',
|
||||
subTitle: inviterCode.isEmpty ? '未设置' : inviterCode,
|
||||
onTap: () {
|
||||
if (inviterCode.isEmpty) {
|
||||
Get.to(() => const InviteBindPage());
|
||||
} else {
|
||||
showToast('您已绑定过邀请码');
|
||||
}
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '账号凭证',
|
||||
onTap: () {
|
||||
Get.to(() => MineAccountIdentityPage());
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '常见问题',
|
||||
onTap: () {
|
||||
Get.to(() => MineQAPage());
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '清除缓存',
|
||||
subTitle: cacheSize,
|
||||
onTap: () async {
|
||||
await VideoDownloadManager.instance.emptyCache();
|
||||
await ImageCacheDisk.emptyCache();
|
||||
await VideoCacheManager().emptyCache();
|
||||
await ImageCacheManager().emptyCache();
|
||||
cacheSize = "0.0KB";
|
||||
if (mounted) setState(() {});
|
||||
showToast('清理缓存成功');
|
||||
}),
|
||||
_buildLine(),
|
||||
_buildItem(
|
||||
title: '检查更新',
|
||||
subTitle: 'V${Config.innerVersion}',
|
||||
onTap: () {
|
||||
//比对启动页拉到的版本配置,确实有新版才弹更新框
|
||||
final target = checkUpdate();
|
||||
target == null
|
||||
? showToast("当前已是最新版!")
|
||||
: UpdateDialog.show(target);
|
||||
}),
|
||||
_buildLine(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLine() {
|
||||
return Padding(padding: EdgeInsets.only(left: 16), child: .5.line);
|
||||
}
|
||||
|
||||
Widget _buildItem({
|
||||
String title = '',
|
||||
String? subTitle,
|
||||
bool showCopy = false,
|
||||
String? avatarUrl,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 56,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9), fontSize: 14),
|
||||
),
|
||||
Spacer(),
|
||||
if (avatarUrl != null)
|
||||
NetworkImageLoader(
|
||||
imageUrl: avatarUrl,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
),
|
||||
if (subTitle != null)
|
||||
Text(
|
||||
subTitle,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.45), fontSize: 12),
|
||||
),
|
||||
if (showCopy) ...[
|
||||
4.sizeBoxW,
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: '${globalStore.meInfo?.uid}'));
|
||||
showToast('复制成功');
|
||||
},
|
||||
child: Image.asset(
|
||||
'mine_copy.png'.mineImgPath,
|
||||
width: 24,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!showCopy) ...[
|
||||
10.sizeBoxW,
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: Color(0xFF70708C),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import 'mine_setting_profile_page.dart';
|
||||
import 'setting_profile_widget.dart';
|
||||
|
||||
abstract class MineSettingProfilePage extends GetxController {
|
||||
String get title => '';
|
||||
|
||||
late final nickNameTfCtr = TextEditingController();
|
||||
late final sloganTfCtr = TextEditingController();
|
||||
|
||||
onSaveProfile();
|
||||
|
||||
instanceChildItem(int index);
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
nickNameTfCtr.dispose();
|
||||
sloganTfCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
class SettingNickNameController extends MineSettingProfilePage {
|
||||
@override
|
||||
String get title => '修改昵称';
|
||||
|
||||
@override
|
||||
onSaveProfile() async {
|
||||
if (nickNameTfCtr.text.isEmpty) {
|
||||
showToast('昵称不能为空~');
|
||||
return;
|
||||
}
|
||||
LoadingHelper.showLoading();
|
||||
final res = await MineService.updateUserInfo({'name': nickNameTfCtr.text});
|
||||
LoadingHelper.dismissLoading();
|
||||
if (res) {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
instanceChildItem(int index) {
|
||||
return SettingNickName();
|
||||
}
|
||||
}
|
||||
|
||||
class SettingSloganController extends MineSettingProfilePage {
|
||||
@override
|
||||
String get title => '个性签名';
|
||||
|
||||
@override
|
||||
onSaveProfile() async {
|
||||
if (sloganTfCtr.text.isEmpty) {
|
||||
showToast('请输入个性签名~');
|
||||
return;
|
||||
}
|
||||
LoadingHelper.showLoading();
|
||||
final res = await MineService.updateUserInfo({'summary': sloganTfCtr.text});
|
||||
LoadingHelper.dismissLoading();
|
||||
if (res) {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
instanceChildItem(int index) {
|
||||
return SettingSlogan();
|
||||
}
|
||||
}
|
||||
|
||||
MineSettingProfilePage instanceSettingProfileController(
|
||||
SettingProfileType type) {
|
||||
switch (type) {
|
||||
case SettingProfileType.nickName:
|
||||
return SettingNickNameController();
|
||||
case SettingProfileType.slogan:
|
||||
return SettingSloganController();
|
||||
default:
|
||||
throw '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import 'mine_setting_profile_logic.dart';
|
||||
|
||||
enum SettingProfileType {
|
||||
nickName,
|
||||
slogan,
|
||||
}
|
||||
|
||||
class SettingProfilePage extends StatelessWidget {
|
||||
final SettingProfileType type;
|
||||
const SettingProfilePage(this.type, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineSettingProfilePage>(
|
||||
init: instanceSettingProfileController(type),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(controller.title),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
onTap: () => controller.onSaveProfile(),
|
||||
child: Text(
|
||||
'保存',
|
||||
style: TextStyle(color: Color(0xff999999), fontSize: 14),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW
|
||||
],
|
||||
),
|
||||
body: controller.instanceChildItem(0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
class SettingAvatarLogic extends ListBaseLogic<String> {
|
||||
int selectIndex = -1;
|
||||
late final int rawIndex;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
void loadData() => fetchData(isRefresh: true, fetch: _fetch);
|
||||
|
||||
//头像列表无分页,单次拉取,hasNext 固定 false
|
||||
Future<(List<String>?, bool)> _fetch(int page) async {
|
||||
final res = await MineService.getPortrait();
|
||||
selectIndex = res.indexOf(globalStore.meInfo?.portrait ?? '');
|
||||
rawIndex = selectIndex;
|
||||
return (res, false);
|
||||
}
|
||||
|
||||
selectAavatar(int index) {
|
||||
selectIndex = index;
|
||||
update();
|
||||
}
|
||||
|
||||
confirmChangeAvatar() async {
|
||||
if (globalStore.meInfo?.urrPortraitStatus == 1) {
|
||||
showToast('头像审核中~');
|
||||
return;
|
||||
}
|
||||
if (selectIndex == rawIndex || selectIndex == -1) {
|
||||
showToast('你都没选择,保存什么~');
|
||||
return;
|
||||
}
|
||||
final res = await MineService.updateUserInfo(
|
||||
{'portrait': dataList![selectIndex], 'isDefaultSource': true});
|
||||
if (res) {
|
||||
globalStore.updateUserInfo();
|
||||
showToast('更新成功');
|
||||
Get.back(result: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'setting_avatar_logic.dart';
|
||||
|
||||
class SettingAvatarPage extends StatelessWidget {
|
||||
const SettingAvatarPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<SettingAvatarLogic>(
|
||||
init: SettingAvatarLogic(),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('选择头像'),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: controller.confirmChangeAvatar,
|
||||
child: Text(
|
||||
'保存',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55), fontSize: 12),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: () {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
if (controller.isEmptyData) return CErrorWidget();
|
||||
final list = controller.dataList!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
30.verticalSpace,
|
||||
NetworkImageLoader(
|
||||
imageUrl: context.watch<GlobalStore>().meInfo?.portrait ?? '',
|
||||
width: 90,
|
||||
height: 90,
|
||||
borderRadius: 45,
|
||||
),
|
||||
10.verticalSpace,
|
||||
Text(
|
||||
'当前头像',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55), fontSize: 12),
|
||||
),
|
||||
20.verticalSpace,
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
padding: EdgeInsets.only(left: 38, right: 38, top: 0),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 20,
|
||||
crossAxisSpacing: 20,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final select = controller.selectIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () => controller.selectAavatar(index),
|
||||
child: Stack(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: list[index],
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
borderRadius: 100,
|
||||
),
|
||||
if (select)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Image.asset(
|
||||
'red_checked.png'.mineImgPath,
|
||||
width: 22,
|
||||
height: 22,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import 'mine_setting_profile_logic.dart';
|
||||
|
||||
class SettingNickName extends StatefulWidget {
|
||||
const SettingNickName({super.key});
|
||||
|
||||
@override
|
||||
State<SettingNickName> createState() => _SettingNickNameState();
|
||||
}
|
||||
|
||||
class _SettingNickNameState extends State<SettingNickName> {
|
||||
late final controller = Get.find<MineSettingProfilePage>();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.nickNameTfCtr.text = globalStore.meInfo?.name ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
60.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller.nickNameTfCtr,
|
||||
maxLength: 20,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9), fontSize: 18),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '输入新的昵称',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35),
|
||||
fontSize: 18),
|
||||
counterText: '',
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.zero),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => controller.nickNameTfCtr.clear(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(90)),
|
||||
child: Image.asset(
|
||||
'close_button.png'.commonImgPath,
|
||||
width: 24,
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
12.sizeBoxH,
|
||||
0.5.line,
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'注意*诱导性昵称会被投诉封号',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35), fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingSlogan extends StatefulWidget {
|
||||
const SettingSlogan({super.key});
|
||||
|
||||
@override
|
||||
State<SettingSlogan> createState() => _SettingSloganState();
|
||||
}
|
||||
|
||||
class _SettingSloganState extends State<SettingSlogan> {
|
||||
late final controller = Get.find<MineSettingProfilePage>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 16, top: 12, right: 16),
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: .04),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
constraints: BoxConstraints(
|
||||
minHeight: 111,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 10,
|
||||
style: const TextStyle(color: Color(0xff333333), fontSize: 12),
|
||||
maxLength: 150,
|
||||
controller: controller.sloganTfCtr,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: '有趣的介绍能让你的逼格提高N个档次!...',
|
||||
hintStyle: TextStyle(color: Color(0xff999999), fontSize: 12),
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
// 字数计数:只监听不接管所有权(ChangeNotifierProvider(create:) 会把 ctr 一起 dispose,
|
||||
// 而它归 MineSettingProfilePage.onClose 释放 → 二次释放)
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.sloganTfCtr,
|
||||
builder: (_, value, __) => Text(
|
||||
'${value.text.length}/150',
|
||||
style: TextStyle(
|
||||
color: Color(0xff666666),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/address.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import '../../../hj_utils/image_util.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'mine_share_record_page.dart';
|
||||
|
||||
//分享邀请
|
||||
|
||||
class MineSharePage extends StatelessWidget {
|
||||
// 该页只通过 Get.to 作为路由根打开,实例稳定不会被父级 rebuild,
|
||||
// boundaryKey 字段只创建一次,RepaintBoundary 截图正常
|
||||
final GlobalKey boundaryKey = GlobalKey();
|
||||
|
||||
MineSharePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("邀请分享"),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
Get.to(() => MineShareRecordPage());
|
||||
},
|
||||
child: Text(
|
||||
"记录",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
// 用 FittedBox 完整渲染卡片,不放进 SingleChildScrollView:
|
||||
// RepaintBoundary 在可滚动容器内时,toImage 受 Viewport 裁剪影响会截图残缺/失败
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: RepaintBoundary(
|
||||
key: boundaryKey,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 30),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
height: 550,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
child: Image.asset('share_bg.webp'.mineImgPath),
|
||||
),
|
||||
// 整张卡片统一响应 meInfo 变化:头像/邀请码/二维码同源刷新
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, provider, __) {
|
||||
final me = provider.meInfo;
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.center,
|
||||
children: [
|
||||
30.sizeBoxH,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(45),
|
||||
border: Border.all(
|
||||
color: Color(0x4DF68804),
|
||||
width: 3),
|
||||
),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: me?.portrait ?? '',
|
||||
width: 82,
|
||||
height: 82,
|
||||
borderRadius: 45),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
'我的邀请码',
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withValues(alpha: .9),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
me?.promotionCode ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withValues(alpha: .55),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
30.sizeBoxH,
|
||||
Text("每邀请3人,送3天VIP",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFFF68804))),
|
||||
10.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(11.w),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"code_bg.webp"
|
||||
.mineImgPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(6.w),
|
||||
color: Colors.white,
|
||||
child: QrImageView(
|
||||
data: me?.promoteURL ?? "",
|
||||
version: QrVersions.auto,
|
||||
size: 100,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'提示*苹果手机请用相机扫码/安卓手机\n推荐UC浏览器扫码',
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withValues(alpha: .55),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Container(
|
||||
height: 30,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 18, vertical: 3),
|
||||
child: Text(
|
||||
'${Config.appName} 官网地址 ${Address.groundUrl ?? ""}',
|
||||
style: TextStyle(
|
||||
color: Colors.white
|
||||
.withValues(alpha: .55),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final ok =
|
||||
await ImageUtil.saveWidgetToAlbum(boundaryKey);
|
||||
showToast(ok ? "保存成功" : "保存失败,请重试");
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
"保存图片",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(
|
||||
text: globalStore.meInfo?.promoteURL ?? ""));
|
||||
showToast('复制成功');
|
||||
},
|
||||
child: Container(
|
||||
width: 112,
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"复制链接",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0x8CFFFFFF),
|
||||
decoration: TextDecoration.underline, // 添加下划线
|
||||
decorationColor: Color(0xFF11887C), // 下划线颜色
|
||||
decorationThickness: 1.0, // 下划线粗细
|
||||
decorationStyle: TextDecorationStyle.solid, // 下划线样式
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:hgdj/hj_model/user/user_income_info_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
|
||||
import '../../../hj_model/mine/promotion_record.dart';
|
||||
|
||||
class MineShareRecordLogic extends ListBaseLogic<Promotion> {
|
||||
UserIncomeModel? model; // 邀请收益头部信息(独立接口,与列表分页无关)
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadInfo();
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 头部邀请收益信息
|
||||
void loadInfo() async {
|
||||
model = await MineService.fetchIncomeInfo();
|
||||
update();
|
||||
}
|
||||
|
||||
// 邀请记录列表:下拉刷新 / 上拉加载更多(分页、防重入、异常兜底交给基类)
|
||||
void loadData({bool isRefresh = true}) =>
|
||||
fetchData(isRefresh: isRefresh, fetch: _fetch);
|
||||
|
||||
Future<(List<Promotion>?, bool)> _fetch(int page) async {
|
||||
final resp = await MineService.getBindRecord(10, page);
|
||||
return (resp?.list, resp?.hasNext == true); // hasNext==true 才有更多(保持原语义)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/mine/promotion_record.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import 'mine_share_record_logic.dart';
|
||||
|
||||
class MineShareRecordPage extends StatelessWidget {
|
||||
const MineShareRecordPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineShareRecordLogic>(
|
||||
init: MineShareRecordLogic(),
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("邀请记录")),
|
||||
body: Column(
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
Text('已邀请:${logic.model?.totalInviteUserCount ?? 0}人',
|
||||
style: textStyle(12, Color(0xff525252), FontWeight.w400)),
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
onLoading: (_) => logic.loadData(isRefresh: false),
|
||||
onRefresh: (_) => logic.loadData(),
|
||||
child: _buildBody(logic),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// loading / 空数据 / 列表 三态
|
||||
Widget _buildBody(MineShareRecordLogic logic) {
|
||||
if (logic.isLoading) return LoadingCenterWidget();
|
||||
if (logic.isEmptyData) return CErrorWidget();
|
||||
final list = logic.dataList ?? [];
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shrinkWrap: true,
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
Divider(height: 0.5, color: Colors.white.withValues(alpha: .1)),
|
||||
itemBuilder: (_, index) => _buildItemView(list[index]),
|
||||
);
|
||||
}
|
||||
|
||||
// 单条邀请记录:头像 + 昵称 + 注册时间
|
||||
Widget _buildItemView(Promotion item) {
|
||||
return Container(
|
||||
height: 30,
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: item.portrait ?? '',
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 30,
|
||||
),
|
||||
11.sizeBoxW,
|
||||
Text(
|
||||
item.name ?? '',
|
||||
style: TextStyle(
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.0),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'注册时间: ${item.createAt.utcToYMD(gap: '/')}',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../tools_base/toast.dart';
|
||||
import '../../../tools_base/video_download/video_save_util.dart';
|
||||
import '../../../tools_base/video_download/video_download_manager.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import '../../home/home_cell_style/video_simple_cell.dart';
|
||||
import 'video_cache_logic.dart';
|
||||
|
||||
class VideoCacheCell extends StatefulWidget {
|
||||
final VideoCacheLogic logic; // 注册局部刷新回调用
|
||||
final VideoModel model;
|
||||
final MediaStyle style; // 影视 / 抖音 / 动漫 / 短剧
|
||||
final bool isEditing; // 缓存编辑状态
|
||||
final GestureTapCallback? onCacheTap;
|
||||
|
||||
/// 点卡片进播放页。必须交给内层 [VideoSimpleCell]——它自带手势且在更深一层,
|
||||
/// 只在外面套 GestureDetector 的话手势竞技场里赢的是它,跳转会走它默认的那套(按时长分长/短视频)
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
const VideoCacheCell({
|
||||
super.key,
|
||||
required this.logic,
|
||||
required this.model,
|
||||
required this.style,
|
||||
this.isEditing = false,
|
||||
this.onCacheTap,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoCacheCell> createState() => _VideoCacheCellState();
|
||||
}
|
||||
|
||||
class _VideoCacheCellState extends State<VideoCacheCell> {
|
||||
/// 覆盖层状态切换动画时长:编辑态/缓存按钮/开始结束/勾选态统一用它
|
||||
static const _switchDuration = Duration(milliseconds: 200);
|
||||
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
bool get isDownloading => model.isDownloading;
|
||||
bool get isFinished => model.progress >= 1;
|
||||
|
||||
String get statusText {
|
||||
if (isFinished) return "已完成";
|
||||
if (isDownloading) return "下载中...";
|
||||
return "已暂停";
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 向 logic 注册局部刷新:下载回调更新 model 后只 setState 本 cell,不再整页 update()
|
||||
widget.logic.registerCellRefresher(model, _refresh);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VideoCacheCell oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// 切换到不同视频时换绑刷新回调。
|
||||
// 用 realVideoUrl 而非 id 比较:本地缓存记录的 id 可能都是 "-1",比不出差异会漏换绑
|
||||
if (oldWidget.model.realVideoUrl != model.realVideoUrl) {
|
||||
widget.logic.unregisterCellRefresher(oldWidget.model, _refresh);
|
||||
widget.logic.registerCellRefresher(model, _refresh);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.logic.unregisterCellRefresher(model, _refresh);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// logic 的下载回调命中本 cell 的 url 时触发:model 已被 logic 更新,这里只需重建
|
||||
void _refresh() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _toggleSelected() {
|
||||
model.isSelected = !model.isSelected;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// 短剧按集卖,而本地记录里的播放地址是当初解锁时拿到的,权益到期后它照样能用
|
||||
/// (m3u8 接口只认登录 token,不认单集权益)。**离线播放刻意不拦**——下载就是为了离线看,
|
||||
/// 点卡片直接放本地文件(见 VideoCachePage._openVideo);这里只管两个会往外扩散的动作:
|
||||
/// 导出相册、续下载
|
||||
Future<bool> _ensureUnlocked() async {
|
||||
if (widget.style != MediaStyle.Drama) return true;
|
||||
final episode = await DramaService.fetchEpisode(model.subid);
|
||||
if (episode?.canPlay == true) return true;
|
||||
showToast("该剧集权益已过期,请重新解锁");
|
||||
return false;
|
||||
}
|
||||
|
||||
void _saveToAlbum() async {
|
||||
if (!await _ensureUnlocked()) return;
|
||||
VideoSaveUtil.instance.convertVideoMp4(
|
||||
model.realVideoUrl,
|
||||
loadInfo: DownloadInfo(status: "1", localPath: model.localPath),
|
||||
isShowLoading: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _onCacheTap() async {
|
||||
if (!await _ensureUnlocked()) return;
|
||||
widget.onCacheTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff151515),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildBody()),
|
||||
_buildStatus(),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildOverlay(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// cell 主体:动漫用 ACG 版本,其它(含短剧)用通用视频 cell
|
||||
Widget _buildBody() {
|
||||
if (widget.style == MediaStyle.Cartoon) {
|
||||
return VideoACGCacheCell(model: model);
|
||||
}
|
||||
final isDrama = widget.style == MediaStyle.Drama;
|
||||
return VideoSimpleCell(
|
||||
videoModel: model,
|
||||
onTap: widget.onTap,
|
||||
textLines: 1,
|
||||
imgBorderRadius: BorderRadius.circular(8),
|
||||
//短剧和「热门短剧」橱窗一个样式:标题是剧名,封面右下角标本条是第几集;
|
||||
//按集卖,整部剧标一个金币/VIP 角标对不上,跟橱窗一样关掉
|
||||
coverRightText: isDrama ? '第${model.episodeNo ?? 1}集' : null,
|
||||
showLevelIcon: !isDrama,
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态文案 + 保存按钮 + 进度条
|
||||
Widget _buildStatus() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 3),
|
||||
child: Row(
|
||||
children: [
|
||||
// Expanded + ellipsis:吸收富余宽度,抖音 3 列窄格里不再右溢出
|
||||
Expanded(
|
||||
child: Text(
|
||||
statusText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0xffd43f61), height: 1.5),
|
||||
),
|
||||
),
|
||||
// 仅下载完成且非动漫显示"保存到相册",避免下载中误触 + 右侧溢出
|
||||
if (widget.style != MediaStyle.Cartoon && isFinished) ...[
|
||||
6.sizeBoxW,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _saveToAlbum,
|
||||
child: const Text(
|
||||
"保存到相册",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFFF68804),
|
||||
height: 1.5,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 14,
|
||||
child: Opacity(
|
||||
opacity: isFinished ? 0 : 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
value: model.progress,
|
||||
backgroundColor: const Color(0xff727272),
|
||||
color: const Color(0xffe75160),
|
||||
),
|
||||
),
|
||||
),
|
||||
5.sizeBoxW,
|
||||
// 固定宽度,避免百分比数字位数变化导致进度条右边跳动
|
||||
SizedBox(
|
||||
width: 46,
|
||||
child: Text(
|
||||
"${(model.progress * 100).toStringAsFixed(1)}%",
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Color(0xffaab2b7), height: 1.2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 覆盖层三态切换(编辑勾选 / 缓存按钮 / 下载完成隐藏):淡入淡出,不再硬跳。
|
||||
/// 三个分支 runtimeType 各不相同,AnimatedSwitcher 自动识别为切换;
|
||||
/// 而分支内部的 isDownloading / selected 变化 type 与 key 都不变,交给各自内层动画,蒙层不跟着闪。
|
||||
Widget _buildOverlay() {
|
||||
return AnimatedSwitcher(
|
||||
duration: _switchDuration,
|
||||
// 显式 expand:改造前蒙层是 StackFit.expand 的直接 child(紧约束铺满 cell),
|
||||
// 套 AnimatedSwitcher 后默认布局会变成松约束按内容撑开,这里保持原来的铺满行为
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 正在淡出的旧层要屏蔽点击:它还在树里,否则切换的这 200 毫秒内
|
||||
// 可能点到已经切走的按钮(刚进编辑模式却点中"开始缓存")
|
||||
...previousChildren.map((c) => IgnorePointer(child: c)),
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
),
|
||||
child: () {
|
||||
if (widget.isEditing) return _buildSelectLayer();
|
||||
if (isFinished) return const SizedBox.shrink();
|
||||
return _buildDownloadButton();
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 开始 / 结束缓存按钮
|
||||
Widget _buildDownloadButton() {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: _onCacheTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 72),
|
||||
height: 42,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
// 开始/结束 图标与文案切换:只动这一行,外层蒙层与按钮底不动。
|
||||
// 两种文案都是 4 个字 + 同宽图标,切换期间两者叠在 Stack 里不会撑宽按钮
|
||||
child: AnimatedSwitcher(
|
||||
duration: _switchDuration,
|
||||
child: Row(
|
||||
key: ValueKey(isDownloading),
|
||||
mainAxisSize: MainAxisSize.min, // 按内容收窄,不再固定 120 宽
|
||||
children: [
|
||||
Image.asset(
|
||||
(isDownloading ? "cache_stop.webp" : "cache_start.webp")
|
||||
.videoPath,
|
||||
width: 10,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
6.sizeBoxW,
|
||||
Text(
|
||||
isDownloading ? "结束缓存" : "开始缓存",
|
||||
style: const TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 编辑态勾选层
|
||||
Widget _buildSelectLayer() {
|
||||
final selected = model.isSelected;
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _toggleSelected,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: const Color(0x99707070),
|
||||
),
|
||||
child: Center(
|
||||
// 勾选态切换:淡入 + 轻微放大,点击有即时反馈(scale 从 .7 起,不从 0 免得过于夸张)
|
||||
child: AnimatedSwitcher(
|
||||
duration: _switchDuration,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: Tween(begin: .7, end: 1.0).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
child: Image.asset(
|
||||
(selected ? "cache_selected.webp" : "cache_unselected.webp")
|
||||
.videoPath,
|
||||
key: ValueKey(selected),
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoACGCacheCell extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const VideoACGCacheCell({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: model.cover ?? "",
|
||||
imgBorderRadius: BorderRadius.circular(12),
|
||||
borderRadius: 4,
|
||||
),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
model.title ?? '',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.white),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'${model.updateDesc} · 共${model.totalEpisode ?? 0}话',
|
||||
textAlign: TextAlign.left,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
model.updateStatus == 2 ? Color(0xff757575) : Color(0xffEEC76B),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../tools_base/video_download/video_cache_store.dart';
|
||||
import '../../../tools_base/video_download/video_download_manager.dart';
|
||||
|
||||
class VideoCacheLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
/// 删除退场动画时长:cell 缩放淡出播完才真删数据,page 侧动画时长与此保持一致
|
||||
static const removeAnimDuration = Duration(milliseconds: 260);
|
||||
|
||||
//顺序与 _cacheMap 一致
|
||||
final tabStyles = const [
|
||||
MediaStyle.Video,
|
||||
MediaStyle.ShortVideo,
|
||||
MediaStyle.Cartoon,
|
||||
MediaStyle.Drama
|
||||
];
|
||||
|
||||
late final tabTitles = tabStyles.map((e) => e.cacheTabTitle).toList();
|
||||
|
||||
//在 onInit 里赋值而不是写成 late final 惰性初始化:那样 onClose 里的 dispose
|
||||
//会成为「首次访问」,在控制器已销毁时才去 createTicker
|
||||
late final TabController tabCtr;
|
||||
|
||||
/// 各类型缓存列表;短剧按**集**存,一部剧会有多条记录
|
||||
final _cacheMap = {
|
||||
MediaStyle.Video: <VideoModel>[],
|
||||
MediaStyle.ShortVideo: <VideoModel>[],
|
||||
MediaStyle.Cartoon: <VideoModel>[],
|
||||
MediaStyle.Drama: <VideoModel>[],
|
||||
};
|
||||
|
||||
List<VideoModel> listOf(MediaStyle style) => _cacheMap[style] ?? const [];
|
||||
|
||||
List<VideoModel> get allVideos =>
|
||||
[for (final list in _cacheMap.values) ...list];
|
||||
|
||||
bool isLoading = true;
|
||||
bool isEditing = false;
|
||||
|
||||
/// 删除流程进行中,防止 await 期间重复点「删除」
|
||||
bool _isDeleting = false;
|
||||
|
||||
/// 正在播退场动画的 item:动画期间仍留在列表里,播完才真删数据
|
||||
final _removingItems = <VideoModel>{};
|
||||
|
||||
bool isRemoving(VideoModel model) => _removingItems.contains(model);
|
||||
|
||||
/// taskKey -> 对应 cell 的局部刷新回调。
|
||||
/// 下载进度不再触发整页 update(),改为只 setState 命中 url 的 cell(局部刷新)。
|
||||
/// 用 Set 兜底同一 url 出现在多个 cell 的极端情况。
|
||||
final _cellRefreshers = <String, Set<VoidCallback>>{};
|
||||
|
||||
/// 下载回调:Android isolate / iOS task 两端都靠它回传,命中哪条只刷哪个 cell
|
||||
late final _callback = DownloadCallback(
|
||||
success: (url) async {
|
||||
final model = _findByUrl(url);
|
||||
model?.loadProgress = "100.00";
|
||||
// 实时完成时 localPath 还没回填(只在进页面 searchInfo 填过一次,那会儿还没下完),
|
||||
// 这里补查一次写回,否则点"保存到相册"会因 localPath 为空报"视频文件不存在"
|
||||
final info = await VideoDownloadManager.instance.searchInfo(url: url);
|
||||
if (info != null) model?.localPath = info.localPath;
|
||||
_refreshCells(url);
|
||||
},
|
||||
fail: (url, error) {
|
||||
_findByUrl(url)?.isLoaderRunning = "0";
|
||||
showToast("缓存加载失败");
|
||||
_refreshCells(url);
|
||||
},
|
||||
progress: (url, progress) {
|
||||
_findByUrl(url)?.loadProgress = progress;
|
||||
_refreshCells(url);
|
||||
},
|
||||
);
|
||||
|
||||
/// cell 挂载时注册自己的局部刷新回调
|
||||
void registerCellRefresher(VideoModel model, VoidCallback refresh) {
|
||||
_cellRefreshers
|
||||
.putIfAbsent(VideoDownloadManager.taskKey(model.realVideoUrl), () => {})
|
||||
.add(refresh);
|
||||
}
|
||||
|
||||
/// cell 卸载 / 换绑视频时注销
|
||||
void unregisterCellRefresher(VideoModel model, VoidCallback refresh) {
|
||||
final key = VideoDownloadManager.taskKey(model.realVideoUrl);
|
||||
final set = _cellRefreshers[key];
|
||||
if (set == null) return;
|
||||
set.remove(refresh);
|
||||
if (set.isEmpty) _cellRefreshers.remove(key);
|
||||
}
|
||||
|
||||
/// 精准刷新某 url 对应的 cell(替代原来下载回调里的全页 update())
|
||||
void _refreshCells(String url) {
|
||||
final set = _cellRefreshers[VideoDownloadManager.taskKey(url)];
|
||||
if (set == null) return;
|
||||
for (final refresh in Set.of(set)) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/// 按 taskKey 在列表里找到对应 model(url 带 token/cdn,直接比会漏匹配)
|
||||
VideoModel? _findByUrl(String url) {
|
||||
final key = VideoDownloadManager.taskKey(url);
|
||||
for (final item in allVideos) {
|
||||
if (VideoDownloadManager.taskKey(item.realVideoUrl) == key) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
tabCtr = TabController(length: tabStyles.length, vsync: this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadData());
|
||||
}
|
||||
|
||||
void _loadData() async {
|
||||
try {
|
||||
for (final entry in _cacheMap.entries) {
|
||||
entry.value
|
||||
..clear()
|
||||
..addAll(
|
||||
await VideoCacheStore.instance.getMovieCacheVideoList(entry.key));
|
||||
}
|
||||
// 本地记录只有 url,进度/暂停态要逐条问下载器;顺带把 _callback 挂上去
|
||||
for (final item in allVideos) {
|
||||
//条目多时这个循环很慢,用户中途退页面就别再往下问:
|
||||
//onClose 已经把回调摘干净了,这里再 searchInfo 会把 _callback 重新挂回单例,留下悬挂回调
|
||||
if (isClosed) return;
|
||||
final info = await VideoDownloadManager.instance.searchInfo(
|
||||
url: item.realVideoUrl,
|
||||
callback: _callback,
|
||||
);
|
||||
if (info != null) {
|
||||
item.localPath = info.localPath;
|
||||
item.loadProgress = info.progress;
|
||||
item.isLoaderRunning = info.isLoaderRunning;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
isLoading = false;
|
||||
if (!isClosed) update();
|
||||
}
|
||||
|
||||
/// 右上角按钮:非编辑态进编辑,编辑态执行删除
|
||||
void editEvent() {
|
||||
if (_isDeleting) return; // 删除动画/落盘期间不响应重复点击
|
||||
if (isEditing) {
|
||||
_deleteSelected();
|
||||
return;
|
||||
}
|
||||
//进编辑先清掉上轮残留的勾选,否则再次进编辑会显示脏选中态
|
||||
//(删除失败时未删项、或退场动画那 260ms 里新勾的项,都会残留 isSelected)
|
||||
for (final item in allVideos) {
|
||||
item.isSelected = false;
|
||||
}
|
||||
isEditing = true;
|
||||
update();
|
||||
}
|
||||
|
||||
/// 先标记被选中项播退场动画,动画播完再真删,避免 cell 硬闪消失
|
||||
void _deleteSelected() async {
|
||||
final deleteList = allVideos.where((e) => e.isSelected).toList();
|
||||
if (deleteList.isEmpty) {
|
||||
isEditing = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
_isDeleting = true;
|
||||
_removingItems.addAll(deleteList);
|
||||
update();
|
||||
try {
|
||||
await Future.delayed(removeAnimDuration);
|
||||
for (final item in deleteList) {
|
||||
await VideoDownloadManager.instance.delete(item.realVideoUrl);
|
||||
}
|
||||
await VideoCacheStore.instance.removeVideoListNoType(deleteList);
|
||||
for (final list in _cacheMap.values) {
|
||||
list.removeWhere(deleteList.contains);
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
showToast("删除失败");
|
||||
} finally {
|
||||
// 必须复位,否则一次异常就把页面卡死:_isDeleting 会让「删除」按钮永久失效,
|
||||
// _removingItems 残留会让那几个 cell 一直保持透明
|
||||
_removingItems.clear();
|
||||
isEditing = false;
|
||||
_isDeleting = false;
|
||||
// 删除期间用户可能已退页面(GetBuilder 会 dispose logic),落盘照做但别再刷 UI
|
||||
if (!isClosed) update();
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始 / 暂停下载
|
||||
void toggleDownload(VideoModel model) async {
|
||||
if (model.isDownloading) {
|
||||
await VideoDownloadManager.instance.pause(model.realVideoUrl);
|
||||
model.isLoaderRunning = "0";
|
||||
} else {
|
||||
// 开始新任务前,先暂停其它正在下载的任务(同时只允许一个下载)
|
||||
for (final item in allVideos) {
|
||||
if (item.isDownloading) {
|
||||
await VideoDownloadManager.instance.pause(item.realVideoUrl);
|
||||
item.isLoaderRunning = "0";
|
||||
}
|
||||
}
|
||||
final result = await VideoDownloadManager.instance.download(
|
||||
url: model.realVideoUrl,
|
||||
callback: _callback,
|
||||
);
|
||||
if (result != null) {
|
||||
debugLog("download movie file: $result");
|
||||
} else {
|
||||
model.isLoaderRunning = "1";
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// 只注销本页自己注册的那个 _callback,绝不能清光单例里的全部回调:
|
||||
// 那会连短视频页 DownloadButton 等别处组件的回调一起清掉,
|
||||
// 表现为「短视频页点下载 → 来缓存页看一眼 → 退回去,那个按钮的进度就再也不动了」。
|
||||
// _callback 是同一个实例注册在多个 url 上(_loadData 逐个 searchInfo 都传了它),故逐个摘除
|
||||
for (final item in allVideos) {
|
||||
VideoDownloadManager.instance
|
||||
.removeCallback(item.realVideoUrl, _callback);
|
||||
}
|
||||
_cellRefreshers.clear();
|
||||
tabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/stagger_in_item.dart';
|
||||
|
||||
import '../../../hj_model/drama_media_info.dart';
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../tools_base/indicator/custom_tab_indicator.dart';
|
||||
import '../../drama/drama_detail_page.dart';
|
||||
import '../../video/simple_video_player_page.dart';
|
||||
import 'video_cache_cell.dart';
|
||||
import 'video_cache_logic.dart';
|
||||
|
||||
class VideoCachePage extends StatelessWidget {
|
||||
const VideoCachePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<VideoCacheLogic>(
|
||||
init: VideoCacheLogic(),
|
||||
global: false,
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text("下载缓存"),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.editEvent(),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
logic.isEditing ? "删除" : "编辑",
|
||||
style: const TextStyle(
|
||||
color: Color(0xff757575),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
],
|
||||
),
|
||||
body: logic.isLoading
|
||||
? LoadingCenterWidget()
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).appBarTheme.backgroundColor,
|
||||
child: TabBar(
|
||||
//tab 等分整宽,不滚动
|
||||
indicator: CustomIndicator(isGradient: true),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelColor: Color(0x8CFFFFFF),
|
||||
unselectedLabelStyle: TextStyle(fontSize: 14),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w500),
|
||||
labelColor: Color(0xE5FFFFFF),
|
||||
tabs: logic.tabTitles
|
||||
.map(
|
||||
(e) => Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 5, 0, 5),
|
||||
child: Text(e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: logic.tabCtr,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: logic.tabCtr,
|
||||
children: [
|
||||
// keepAlive:切 tab 不重建,保留滚动位置,也避免入场动画反复重播
|
||||
for (final style in logic.tabStyles)
|
||||
_buildGrid(logic, style).keepAlive,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGrid(VideoCacheLogic logic, MediaStyle style) {
|
||||
final dataArr = logic.listOf(style);
|
||||
if (dataArr.isEmpty) {
|
||||
return CErrorWidget();
|
||||
}
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//短剧跟「热门短剧」橱窗同规格:2 列 168/266
|
||||
crossAxisCount:
|
||||
style == MediaStyle.ShortVideo || style == MediaStyle.Cartoon
|
||||
? 3
|
||||
: 2,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: switch (style) {
|
||||
MediaStyle.ShortVideo => 191 / 390,
|
||||
MediaStyle.Cartoon => 191 / 420,
|
||||
MediaStyle.Drama => 168 / 266,
|
||||
_ => 191 / 210,
|
||||
},
|
||||
),
|
||||
itemCount: dataArr.length,
|
||||
itemBuilder: (context, int index) {
|
||||
final videoModel = dataArr[index];
|
||||
return StaggerInItem(
|
||||
// key 用 model 对象身份而非 index:删除后剩余项 Element 按 key 复用、
|
||||
// 各自 State 保留,既不重播入场动画(否则整屏闪一下),也不会串味。
|
||||
// 用 ObjectKey 而不是 url/taskKey:同一 url 可能对应多条记录(见 _cellRefreshers 的注释),
|
||||
// 那样会撞成重复 key 直接抛 Duplicate keys;对象身份天然唯一且删除后不变
|
||||
key: ObjectKey(videoModel),
|
||||
index: index,
|
||||
child: _buildCell(logic, videoModel, style),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 单个缓存 cell:被标记删除时缩放 + 淡出(时长与 logic 一致,播完 logic 才真删数据)
|
||||
Widget _buildCell(
|
||||
VideoCacheLogic logic, VideoModel videoModel, MediaStyle style) {
|
||||
final isRemoving = logic.isRemoving(videoModel);
|
||||
// 退场中屏蔽点击:opacity 到 0 也照样命中手势,否则那 260 毫秒里
|
||||
// 点到看不见的 cell 会跳进播放页
|
||||
return IgnorePointer(
|
||||
ignoring: isRemoving,
|
||||
child: AnimatedOpacity(
|
||||
opacity: isRemoving ? 0 : 1,
|
||||
duration: VideoCacheLogic.removeAnimDuration,
|
||||
curve: Curves.easeOut,
|
||||
child: AnimatedScale(
|
||||
scale: isRemoving ? .8 : 1,
|
||||
duration: VideoCacheLogic.removeAnimDuration,
|
||||
curve: Curves.easeOut,
|
||||
child: GestureDetector(
|
||||
//动漫 cell 自身没有手势,靠这一层接管;其余 cell 内层 VideoSimpleCell 也收了同一个回调
|
||||
onTap: () => _openVideo(videoModel, style),
|
||||
child: VideoCacheCell(
|
||||
logic: logic,
|
||||
model: videoModel,
|
||||
style: style,
|
||||
isEditing: logic.isEditing,
|
||||
onCacheTap: () => logic.toggleDownload(videoModel),
|
||||
onTap: () => _openVideo(videoModel, style),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 点缓存卡片进播放页
|
||||
void _openVideo(VideoModel videoModel, MediaStyle style) {
|
||||
//下载就是为了离线看:有本地文件就直接放,不进二级页——二级页是纯网络播放器
|
||||
//(VideoPlayerBaseLogic.initPlayer 只认 network),进那儿等于没用上下好的文件,断网还打不开
|
||||
if (style == MediaStyle.Drama && videoModel.localPath?.isNotEmpty == true) {
|
||||
_playLocal(videoModel,
|
||||
'${videoModel.title ?? ""} 第${videoModel.episodeNo ?? 1}集');
|
||||
return;
|
||||
}
|
||||
//没下完(或只有下载态没落到文件)的没有本地文件可放,进二级页在线看这一集:
|
||||
//本地记录只留了剧 id 和分集 id,短剧不在 /vid/info 里,走 pushToVideoPage 那套只会拿剧 id 去查视频详情
|
||||
if (style == MediaStyle.Drama) {
|
||||
Get.to(() => DramaDetailPage(
|
||||
drama: DramaMediaInfo()
|
||||
..id = videoModel.id
|
||||
..title = videoModel.title
|
||||
..verticalCover = videoModel.cover,
|
||||
initialContentId: videoModel.subid,
|
||||
));
|
||||
return;
|
||||
}
|
||||
//id 为 -1 的是老版本落的记录,没有详情可拉,只能本地放
|
||||
if (videoModel.id == "-1") {
|
||||
_playLocal(videoModel, videoModel.title ?? "");
|
||||
return;
|
||||
}
|
||||
pushToVideoPage(
|
||||
videoModel: videoModel, isCartoon: videoModel.videoType == 1);
|
||||
}
|
||||
|
||||
/// 用本地文件播([SimpleVideoPlayerLogic] 拿到 localPath 就走 PlayerFactory.file)
|
||||
void _playLocal(VideoModel videoModel, String title) =>
|
||||
Get.to(SimpleVideoPlayerPage(
|
||||
localPath: videoModel.localPath,
|
||||
videoUrl: videoModel.realVideoUrl,
|
||||
title: title,
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/gradient_text.dart';
|
||||
|
||||
class UserChargeView extends StatelessWidget {
|
||||
final bool isFromCoin; // true: 来源金币充值, vip时间为灰色
|
||||
/// true:旧版文案/配色(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const UserChargeView(
|
||||
{super.key, this.isFromCoin = false, this.classic = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<GlobalStore>(
|
||||
builder: (_, provider, __) {
|
||||
if (classic) return _buildClassic(provider);
|
||||
return _buildModern(provider);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModern(GlobalStore provider) {
|
||||
final vipIcon = provider.meInfo?.vipImageName ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: provider.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
provider.meInfo == null
|
||||
? '未知'
|
||||
: provider.meInfo?.name?.substring(
|
||||
0, min(provider.meInfo?.name?.length ?? 0, 9)) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Color(0xffF6EEDC),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
if (!provider.isVIP)
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Color(0x14FFFFFF), width: 1),
|
||||
),
|
||||
child: const Text(
|
||||
'未开通',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (vipIcon.isNotEmpty)
|
||||
Image.asset(vipIcon, height: 20),
|
||||
],
|
||||
),
|
||||
4.sizeBoxH,
|
||||
globalStore.isVIP
|
||||
? Consumer<PreSaleProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (isFromCoin) {
|
||||
return Text(
|
||||
'会员到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GradientText(
|
||||
'会员到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xffFFE8BE),
|
||||
Color(0xffE6B764),
|
||||
],
|
||||
),
|
||||
style: TextStyle(fontSize: 12),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
'开通会员 · 解锁全站尊享特权',
|
||||
style: TextStyle(
|
||||
color: Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16);
|
||||
}
|
||||
|
||||
Widget _buildClassic(GlobalStore provider) {
|
||||
final vipIcon = provider.meInfo?.vipImageName ?? '';
|
||||
return Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: provider.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
provider.meInfo == null
|
||||
? '未知'
|
||||
: provider.meInfo?.name?.substring(
|
||||
0, min(provider.meInfo?.name?.length ?? 0, 9)) ??
|
||||
'',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
if (provider.isVIP && vipIcon.isNotEmpty)
|
||||
Image.asset(vipIcon, height: 20),
|
||||
],
|
||||
),
|
||||
4.sizeBoxH,
|
||||
globalStore.isVIP
|
||||
? Consumer<PreSaleProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (isFromCoin) {
|
||||
return Text(
|
||||
'到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return GradientText(
|
||||
'到期时间:${globalStore.meInfo?.vipExpireDate?.utcToYMD()}',
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xffFFE8BE),
|
||||
Color(0xffE6B764),
|
||||
],
|
||||
),
|
||||
style: TextStyle(fontSize: 12),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
'您还不是会员 开通会员 畅享特权',
|
||||
style: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
class AICouponModel {
|
||||
String? createTime;
|
||||
String? expiredTime;
|
||||
String? goodsDesc;
|
||||
String? goodsName;
|
||||
String? goodsOrigin;
|
||||
int? goodsType;
|
||||
int? goodsValue;
|
||||
String? id;
|
||||
int? status;
|
||||
int? uid;
|
||||
String? useTime;
|
||||
|
||||
AICouponModel(
|
||||
{this.createTime,
|
||||
this.expiredTime,
|
||||
this.goodsDesc,
|
||||
this.goodsName,
|
||||
this.goodsOrigin,
|
||||
this.goodsType,
|
||||
this.goodsValue,
|
||||
this.id,
|
||||
this.status,
|
||||
this.uid,
|
||||
this.useTime});
|
||||
|
||||
AICouponModel.fromJson(Map<String, dynamic> json) {
|
||||
createTime = json['createTime'];
|
||||
expiredTime = json['expiredTime'];
|
||||
goodsDesc = json['goodsDesc'];
|
||||
goodsName = json['goodsName'];
|
||||
goodsOrigin = json['goodsOrigin'];
|
||||
goodsType = json['goodsType'];
|
||||
goodsValue = json['goodsValue'];
|
||||
id = json['id'];
|
||||
status = json['status'];
|
||||
uid = json['uid'];
|
||||
useTime = json['useTime'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['createTime'] = this.createTime;
|
||||
data['expiredTime'] = this.expiredTime;
|
||||
data['goodsDesc'] = this.goodsDesc;
|
||||
data['goodsName'] = this.goodsName;
|
||||
data['goodsOrigin'] = this.goodsOrigin;
|
||||
data['goodsType'] = this.goodsType;
|
||||
data['goodsValue'] = this.goodsValue;
|
||||
data['id'] = this.id;
|
||||
data['status'] = this.status;
|
||||
data['uid'] = this.uid;
|
||||
data['useTime'] = this.useTime;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../../../hj_model/mine/exchange/recharge_list_model.dart';
|
||||
import '../../../hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
|
||||
class MineChargeCoinLogic extends GetxController {
|
||||
/// 下单来源(透传 /mine/topay 的 sourcePage),由入口传入
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 下单埋点上下文(短剧付费墙要带 mediaId/contentId/checkoutContextId 做归因)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
MineChargeCoinLogic(
|
||||
{this.sourcePage = PaySourcePage.unknown, this.orderTrack});
|
||||
|
||||
bool isInitLoading = true; // 首屏加载中
|
||||
RechargeListModel? model; // 金币充值档位列表
|
||||
RechargeTypeModel? selectedCoin; // 当前选中的金币档位
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
// 拉取金币充值档位,默认选中第一档
|
||||
Future<void> loadData() async {
|
||||
final res = await MineService.getChatRechargeTypes(1);
|
||||
isInitLoading = false;
|
||||
model = res;
|
||||
res?.list ??= [];
|
||||
if (res?.list?.isNotEmpty == true) {
|
||||
selectedCoin = res!.list!.first;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// 选择金币档位
|
||||
void onSelectCoin(int index) {
|
||||
selectedCoin = model?.list?[index];
|
||||
update();
|
||||
}
|
||||
|
||||
// 去支付(购买金币)
|
||||
void onGotoPay() {
|
||||
if (selectedCoin == null) {
|
||||
showToast("请选择产品");
|
||||
return;
|
||||
}
|
||||
if (selectedCoin!.rechargeTypeListUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
Get.bottomSheet(
|
||||
OnlinePayPage(
|
||||
coinRcModel: selectedCoin,
|
||||
orderTrack: (orderTrack ?? const PayOrderTrackInfo())
|
||||
.copyWith(sourcePage: sourcePage),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../make_money/mine_withdrawal_record_page.dart';
|
||||
import 'charge_user_header.dart';
|
||||
import 'mine_charge_coin_logic.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'widgets/coin_item.dart';
|
||||
|
||||
//金币充值页面
|
||||
class MineChargeCoinPage extends StatelessWidget {
|
||||
/// 下单来源,入口不传则 UNKNOWN(金币没有专属来源枚举,别拿会员的顶上)
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
const MineChargeCoinPage(
|
||||
{super.key, this.sourcePage = PaySourcePage.unknown});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeCoinLogic>(
|
||||
init: MineChargeCoinLogic(sourcePage: sourcePage),
|
||||
global: false, // 多入口各自独立:叠栈时别复用上一个页面的 controller(来源会串成上一次的)
|
||||
builder: (controller) {
|
||||
return Scaffold(
|
||||
body: () {
|
||||
if (controller.isInitLoading) return const LoadingCenterWidget();
|
||||
if (controller.model == null)
|
||||
return CErrorWidget(retryOnTap: () => controller.loadData());
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
// 头像 + 余额卡片:随金币列表上滑,自带折叠淡出动画
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
backgroundColor: Color(0xff0F0F0F),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
expandedHeight: kToolbarHeight + 200,
|
||||
title: Text(
|
||||
'金币充值',
|
||||
style: textStyle(16, Colors.white, FontWeight.w600),
|
||||
),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.recharge)),
|
||||
child: Text(
|
||||
'充值记录',
|
||||
style: TextStyle(
|
||||
color: Color(0xff666666), fontSize: 16.sp),
|
||||
),
|
||||
),
|
||||
18.w.sizeBoxW
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: [
|
||||
kToolbarHeight.sizeBoxH,
|
||||
UserChargeView(isFromCoin: true),
|
||||
18.sizeBoxH,
|
||||
_buildWallet(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverGrid.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: 111 / 138,
|
||||
),
|
||||
itemCount: controller.model?.list?.length ?? 0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = controller.model!.list![index];
|
||||
final isSelected =
|
||||
controller.selectedCoin?.id == model.id;
|
||||
return CoinItem(
|
||||
model,
|
||||
onTap: () => controller.onSelectCoin(index),
|
||||
isSelected: isSelected,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: 18.sizeBoxH),
|
||||
SliverToBoxAdapter(
|
||||
child: EasyRichText(
|
||||
'*如提示【交易失败】【账户风险】等,可重新发起订单,或在15分钟后重试支付。如支付未到账,请反馈客服订单号',
|
||||
defaultStyle:
|
||||
TextStyle(color: Color(0xff666666), fontSize: 12),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '反馈客服订单号',
|
||||
style: TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontWeight: FontWeight.w500),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildBottomBar(controller),
|
||||
],
|
||||
);
|
||||
}(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 底部支付按钮 + 客服入口
|
||||
Widget _buildBottomBar(MineChargeCoinLogic controller) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => controller.onGotoPay(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'¥${controller.selectedCoin?.moneyYuan ?? 0}/立即支付',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('支付问题反馈,点击联系 ',
|
||||
style: textStyle(12, Color(0xffBFBFC1), FontWeight.w400)),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
pushToCustomService();
|
||||
},
|
||||
child: Text('在线客服',
|
||||
style: textStyle(12, Color(0xffFFD460), FontWeight.w400)),
|
||||
)
|
||||
],
|
||||
),
|
||||
// 垫上虚拟导航栏高度,避免底部内容被遮挡(edge-to-edge)
|
||||
(16 + screen.paddingBottom).sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 我的金币余额卡片
|
||||
Widget _buildWallet() {
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: EdgeInsets.fromLTRB(16, 18, 16, 18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'我的金币余额',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxW,
|
||||
Consumer<GlobalStore>(builder: (_, store, __) {
|
||||
final wallet = store.wallet;
|
||||
final total = (wallet?.amount ?? 0) + (wallet?.income ?? 0);
|
||||
return Text(
|
||||
"$total",
|
||||
style: TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600),
|
||||
);
|
||||
}),
|
||||
Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.bill), opaque: false),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 30,
|
||||
width: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFFD460),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'余额明细',
|
||||
style: TextStyle(color: Color(0xff3D3D3D), fontSize: 14),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import 'package:carousel_slider/carousel_controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/alert/vip_guide/guide_manager.dart';
|
||||
import 'package:hgdj/alert/vip_guide/timed_popup_manager.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../alert/vip_guide/guide_countdown_dialog.dart';
|
||||
import '../../../alert/vip_guide/guide_config.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_card_item.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员卡页/购买弹窗的页面级 Logic:只管「当前选中卡 / 轮播 / 支付触发」。
|
||||
/// 会员卡列表数据(请求/缓存/组装/选卡)统一在 [VipProductManager],UI 用 Consumer 消费。
|
||||
class MineChargeVipLogic extends GetxController {
|
||||
/// 局部刷新 id:只有「跟随选中卡变化」的区域订阅它(卡片选中态 / 价格 / 权益区 / 支付按钮),
|
||||
/// 滑卡时不必整页重建(背景图、轮播本体、用户信息条、AppBar 都跟选中卡无关)
|
||||
static const kSelection = 'vip_selection';
|
||||
|
||||
/// 跳转指定选中的会员卡ID(构造传入,替代 Get.arguments)
|
||||
final String? vipID;
|
||||
|
||||
/// 下单来源(透传 /mine/topay 的 sourcePage)
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 播放页拉起时带上在看的那条内容 id,服务端按它归因;会员中心进来为空
|
||||
final String? videoId;
|
||||
|
||||
/// 入口透传的整份下单上下文,字段比 [videoId] 全时以它为准(短剧付费墙带 mediaId/contentId/checkoutContextId)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
MineChargeVipLogic(
|
||||
{this.vipID,
|
||||
this.sourcePage = PaySourcePage.vipCenter,
|
||||
this.videoId,
|
||||
this.orderTrack});
|
||||
|
||||
/// 当前选中的会员卡(页面级状态,多入口各自独立)
|
||||
VipProductModel? currentProductModel;
|
||||
|
||||
/// 引导弹窗点「立即开通」指定的卡ID,优先级高于构造传入的 vipID(用户显式选择)
|
||||
String? _guideCardId;
|
||||
|
||||
/// 会员卡轮播控制器(页面级,两个入口不能共享)
|
||||
late final pageCtr = CarouselSliderController();
|
||||
|
||||
/// 本页 VIP 卡片统计会话
|
||||
VipCardAnalyticsSession? _analytics;
|
||||
|
||||
//轮播视口占比 / 高度:B 组固定卡面 148×157 + 横向间距 10;高度含角标与底部发光
|
||||
double get itemRatio => vipProductManager.isNewVipUi
|
||||
? (148 + 10) / screen.screenWidth
|
||||
: 137.6 / screen.screenWidth;
|
||||
|
||||
double get cardHeight => vipProductManager.isNewVipUi
|
||||
? 189 // 8角标 + 157卡面 + 24底部 Glow
|
||||
: (screen.screenWidth * itemRatio) * 150 / 110.6;
|
||||
|
||||
/// 进页面是否强制拉最新:会员中心页 true(每次刷新),购买弹窗覆写为 false(优先用缓存)
|
||||
bool get preferFreshData => true;
|
||||
|
||||
/// 进入本页是否触发「优惠倒计时」引导弹窗(VIP_CENTER)。购买弹窗(BuyVipAlertLogic)覆写为 false,避免叠弹。
|
||||
bool get enableEntryGuidePopup => true;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
PreSaleProvider().refreshConfig();
|
||||
loadCards(force: preferFreshData);
|
||||
if (enableEntryGuidePopup) _tryShowEntryGuide();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// 页面/弹窗销毁 → GetBuilder 按 tag 走 Get.delete → onDelete → 这里补报 CLOSE
|
||||
_analytics?.reportCloseWithoutPurchaseIfNeeded();
|
||||
_analytics = null;
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// VIP_CENTER:开关允许时**每次**进会员页都弹优惠倒计时引导
|
||||
/// (含从别的引导弹窗点「开通」跳进来的那次,运营要追单,不做去重)。
|
||||
void _tryShowEntryGuide() {
|
||||
TimedPopupManager().trigger(
|
||||
canShow: () => GuideManager().canShow(GuideScene.vipCenter),
|
||||
onShow: () => GuideCountdownDialog.show(
|
||||
scene: GuideScene.vipCenter,
|
||||
// 已在会员页:关弹窗 + 就地选中弹窗配置的那张卡,让底部支付按钮直接对上该卡价格
|
||||
onConfirm: (card) {
|
||||
Get.back(result: true); // 对齐 show() 的返回语义:true=点了开通
|
||||
_selectCard(card);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 选中并滚到指定会员卡:只驱动轮播,选中态由 [onPageChanged] 统一更新(和点卡片一致)。
|
||||
/// 列表还在加载时轮播没渲染、动不了,先记下 id 交给 [_syncSelection] 落位
|
||||
void _selectCard(VipProductModel card) {
|
||||
_guideCardId = card.productID;
|
||||
if (vipProductManager.isLoading || !pageCtr.ready) return;
|
||||
final index = vipProductManager.vipCards
|
||||
.indexWhere((e) => e.productID == card.productID);
|
||||
if (index >= 0) pageCtr.animateToPage(index, curve: Curves.fastOutSlowIn);
|
||||
}
|
||||
|
||||
/// 拉取会员卡列表并同步选中态
|
||||
Future<void> loadCards({bool force = false}) async {
|
||||
await vipProductManager.loadVipCards(force: force);
|
||||
_syncSelection();
|
||||
}
|
||||
|
||||
/// 数据就绪后按优先级确定默认选中卡,并滚动到对应位置;UI 展示后再上报曝光
|
||||
void _syncSelection() {
|
||||
currentProductModel = vipProductManager.defaultVipCard(
|
||||
vipID: _guideCardId ?? vipID, current: currentProductModel);
|
||||
final pid = currentProductModel?.productID;
|
||||
final index = pid == null
|
||||
? -1
|
||||
: vipProductManager.vipCards.indexWhere((e) => e.productID == pid);
|
||||
// 会话尽早创建:避免仅在 post-frame 里建,用户快速返回时 onClose 拿不到 session、CLOSE 丢报。
|
||||
// 必须等有卡数据再建:首次请求失败时卡列表还是空,会把「无实验」快照锁进 session,
|
||||
// 之后错误页重试成功也不会上报(快照 late final 不可变)。
|
||||
if (vipProductManager.vipCards.isNotEmpty)
|
||||
_analytics ??= VipCardAnalyticsSession();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isClosed) return;
|
||||
// 卡列表为空时页面渲染的是错误页,轮播压根没 build,controller 没 attach,
|
||||
// 而 jumpToPage 内部是 _state!.pageController!.page! 三层空断言,会直接崩
|
||||
if (index >= 0 && pageCtr.ready) pageCtr.jumpToPage(index);
|
||||
// 曝光必须在 UI 实际展示后上报
|
||||
_analytics?.reportPageViewAfterPaint();
|
||||
_analytics?.reportProductImpression(currentProductModel,
|
||||
afterPaint: true);
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
Widget instanceChildItem(int index) {
|
||||
final data = vipProductManager.vipCards;
|
||||
if (index > data.length - 1) {
|
||||
return SizedBox(
|
||||
height: cardHeight,
|
||||
width: screen.screenWidth,
|
||||
);
|
||||
}
|
||||
final model = data[index];
|
||||
final card = VipCardItem(
|
||||
model,
|
||||
isSelect: currentProductModel?.productID != null &&
|
||||
currentProductModel?.productID == model.productID,
|
||||
callback: () {
|
||||
pageCtr.animateToPage(index, curve: Curves.fastOutSlowIn);
|
||||
},
|
||||
);
|
||||
// B 组:卡槽 = 148 + 右侧 10 间距
|
||||
if (vipProductManager.isNewVipUi) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Align(alignment: Alignment.centerLeft, child: card),
|
||||
);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
|
||||
/// 滑动切卡
|
||||
onPageChanged(int index) {
|
||||
final data = vipProductManager.vipCards;
|
||||
if (data.isEmpty) return;
|
||||
// 轮播 itemCount = 卡数+2(尾部占位,让最后一张能滑到最左)。padEnds:false 时 PageView 的
|
||||
// 最大页码 = itemCount - 1/viewportFraction,B 组卡槽 158 在 <395 宽的机型上会四舍五入到
|
||||
// 占位项索引,直接取 dataSource[index] 会越界,这里钳到最后一张真实卡。
|
||||
currentProductModel = data[index.clamp(0, data.length - 1)];
|
||||
_analytics?.reportProductImpression(currentProductModel);
|
||||
update([kSelection]);
|
||||
}
|
||||
|
||||
onInitiatePayAction() async {
|
||||
if (currentProductModel == null) {
|
||||
showToast("请选择充值的会员卡~");
|
||||
return;
|
||||
}
|
||||
if (currentProductModel!.rchgTypeUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
await Get.bottomSheet(
|
||||
OnlinePayPage(
|
||||
vipProductModel: currentProductModel,
|
||||
orderTrack: buildOrderTrack(),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
await PreSaleProvider().refreshConfig();
|
||||
update();
|
||||
}
|
||||
|
||||
/// 下单埋点:来源 + 卡皮 session + ACTIVE 时的实验字段(普通卡 / 预售卡共用)
|
||||
PayOrderTrackInfo buildOrderTrack() {
|
||||
final base = orderTrack;
|
||||
return PayOrderTrackInfo(
|
||||
sourcePage: base?.sourcePage ?? sourcePage,
|
||||
sourceRef: base?.sourceRef ?? videoId,
|
||||
videoId: base?.videoId ?? videoId,
|
||||
mediaId: base?.mediaId,
|
||||
contentId: base?.contentId,
|
||||
checkoutContextId: base?.checkoutContextId,
|
||||
sessionId: base?.sessionId ?? _analytics?.sessionId,
|
||||
experimentId: vipProductManager.isExperimentActive
|
||||
? (currentProductModel?.experimentId ??
|
||||
vipProductManager.experimentId)
|
||||
: null,
|
||||
experimentVariant: vipProductManager.isExperimentActive
|
||||
? (currentProductModel?.variant ?? vipProductManager.variant)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../pre_sale/pre_sale_entry.dart';
|
||||
import '../make_money/mine_withdrawal_record_page.dart';
|
||||
import 'charge_user_header.dart';
|
||||
import 'mine_charge_vip_logic.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_item.dart';
|
||||
import 'vip_pay_button.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
import 'vip_ui_kit.dart';
|
||||
|
||||
class MineChargeVipPage extends StatefulWidget {
|
||||
final String? vipID; //跳转指定选中的会员卡ID
|
||||
/// 下单来源,默认会员中心
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
const MineChargeVipPage({
|
||||
super.key,
|
||||
this.vipID,
|
||||
this.sourcePage = PaySourcePage.vipCenter,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MineChargeVipPage> createState() => _MineChargeVipPageState();
|
||||
}
|
||||
|
||||
// 多入口可叠栈,用 per-实例唯一 tag 隔离 controller,避免轮播/选中卡/onReady 串味
|
||||
class _MineChargeVipPageState extends State<MineChargeVipPage>
|
||||
with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
init: MineChargeVipLogic(
|
||||
vipID: widget.vipID, sourcePage: widget.sourcePage),
|
||||
builder: (logic) => Consumer<VipProductManager>(
|
||||
builder: (_, mgr, __) {
|
||||
// ACTIVE+B → 改版 UI;A / DISABLED / 无实验 → 旧版 UI
|
||||
final useNew = mgr.isNewVipUi;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: useNew ? const Color(0xff0F0F0F) : null,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: IconThemeData(color: Colors.white),
|
||||
title: Text('会员中心', style: TextStyle(color: Colors.white)),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(RecordsPage(RecordType.recharge)),
|
||||
child: Text('充值记录',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontSize: 12)),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: useNew
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildPageBackground(mgr),
|
||||
),
|
||||
_buildBody(logic, classic: false),
|
||||
],
|
||||
)
|
||||
: _buildBody(logic, classic: true),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// B 组页顶背景:优先 uiConfig.backgroundImage,空则本地默认图
|
||||
Widget _buildPageBackground(VipProductManager mgr) {
|
||||
final url = mgr.vipBgImage;
|
||||
if (url != null) {
|
||||
return NetworkImageLoader(
|
||||
imageUrl: url,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
return Image.asset(
|
||||
'mine_vip_bg.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
alignment: Alignment.topCenter,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(MineChargeVipLogic logic, {required bool classic}) {
|
||||
if (vipProductManager.isLoading) return LoadingCenterWidget();
|
||||
if (vipProductManager.vipCards.isEmpty) {
|
||||
return CErrorWidget(retryOnTap: () => logic.loadCards(force: true));
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
(kToolbarHeight + screen.paddingTop).sizeBoxH,
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
UserChargeView(classic: classic),
|
||||
26.sizeBoxH,
|
||||
_buildCardSlider(logic),
|
||||
if (vipProductManager.presaleGroup != null) ...[
|
||||
20.sizeBoxH,
|
||||
PreSaleVipEntry().paddingSymmetric(horizontal: 16),
|
||||
],
|
||||
// B:卡皮与核心权益标题间距略收;A 保持原 14
|
||||
(classic ? 14 : 4).sizeBoxH,
|
||||
_buildPrivileges(classic: classic),
|
||||
100.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0, left: 0, right: 0, child: _buildPay(classic: classic)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//会员卡轮播
|
||||
Widget _buildCardSlider(MineChargeVipLogic logic) {
|
||||
return CarouselSlider.builder(
|
||||
carouselController: logic.pageCtr,
|
||||
itemCount: vipProductManager.vipCards.length + 2,
|
||||
//选中态跟着 kSelection 单卡刷新,避免滑一次卡把整个轮播和页面重建一遍
|
||||
itemBuilder: (_, index, __) => GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (l) => l.instanceChildItem(index),
|
||||
),
|
||||
options: CarouselOptions(
|
||||
height: logic.cardHeight,
|
||||
viewportFraction: logic.itemRatio,
|
||||
enableInfiniteScroll: false,
|
||||
enlargeCenterPage: false,
|
||||
padEnds: false,
|
||||
// 选中卡底部 VIP/Glow Gold 不被 PageView 裁切
|
||||
clipBehavior: Clip.none,
|
||||
onPageChanged: (index, __) => logic.onPageChanged(index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//权益区:跟随选中卡,订阅 kSelection 局部刷新;newPrivilege 只遍历一次按 isCore 分组
|
||||
Widget _buildPrivileges({required bool classic}) {
|
||||
return GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (logic) {
|
||||
final privileges = logic.currentProductModel?.newPrivilege ?? [];
|
||||
final coreList = <NewPrivilege>[];
|
||||
final moreList = <NewPrivilege>[];
|
||||
for (final p in privileges) {
|
||||
(p.isCore == true ? coreList : moreList).add(p);
|
||||
}
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
classic
|
||||
? _buildClassicCorePrivileges(coreList)
|
||||
: _buildCorePrivileges(coreList),
|
||||
classic
|
||||
? _buildClassicMorePrivileges(moreList)
|
||||
: _buildMorePrivileges(moreList),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//核心权益:横向滚动卡片(左图标 + 右文案)—— B 改版
|
||||
Widget _buildCorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
const VipCoreSectionTitleImage(),
|
||||
16.sizeBoxH,
|
||||
SizedBox(
|
||||
height: 92,
|
||||
child: ListView.separated(
|
||||
clipBehavior: Clip.none,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.fromLTRB(16, 2, 16, 26),
|
||||
itemCount: list.length,
|
||||
separatorBuilder: (_, __) => 8.sizeBoxW,
|
||||
itemBuilder: (_, index) => VipCorePrivilegeCard(list[index]),
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//核心权益:4 列金边方卡 —— A / DISABLED 旧版
|
||||
Widget _buildClassicCorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
const VipSectionTitle("我的核心权益", classic: true),
|
||||
16.sizeBoxH,
|
||||
GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) => FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: SizedBox(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: VipCorePrivilegeCard(list[index], classic: true)),
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//更多权益:深色圆角容器 + 四列网格 + 展开/收起 —— B 改版
|
||||
Widget _buildMorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return _VipMorePrivilegesPanel(
|
||||
list: list,
|
||||
title: const VipMoreSectionTitleImage(),
|
||||
);
|
||||
}
|
||||
|
||||
//会员特权:直接四列网格 —— A / DISABLED 旧版
|
||||
Widget _buildClassicMorePrivileges(List<NewPrivilege> list) {
|
||||
if (list.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
const VipSectionTitle("我的会员特权", classic: true),
|
||||
12.sizeBoxH,
|
||||
GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 18,
|
||||
childAspectRatio: 63 / 100,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) =>
|
||||
VipProductPrivilegeItem(list[index], classic: true),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//底部支付区域:只有按钮跟选中卡走,渐变底和客服文案是静态的
|
||||
Widget _buildPay({required bool classic}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.black.withValues(alpha: 0), Colors.black],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
GetBuilder<MineChargeVipLogic>(
|
||||
tag: uniqueTag,
|
||||
id: MineChargeVipLogic.kSelection,
|
||||
builder: (logic) => VipPayButton(logic, classic: classic),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
EasyRichText(
|
||||
'支付问题反馈,点击联系 在线客服',
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '在线客服',
|
||||
style: TextStyle(color: Color(0xFFFFD460), fontSize: 12),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
)
|
||||
],
|
||||
defaultStyle: TextStyle(color: Color(0xffDCDCDC), fontSize: 12),
|
||||
),
|
||||
// 垫上虚拟导航栏高度,避免底部内容被遮挡(edge-to-edge)
|
||||
(12 + screen.paddingBottom).sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更多权益:深色圆角面板 + 四列网格,默认展开,可收起为 2 行
|
||||
class _VipMorePrivilegesPanel extends StatefulWidget {
|
||||
final List<NewPrivilege> list;
|
||||
final Widget title;
|
||||
|
||||
const _VipMorePrivilegesPanel({required this.list, required this.title});
|
||||
|
||||
@override
|
||||
State<_VipMorePrivilegesPanel> createState() =>
|
||||
_VipMorePrivilegesPanelState();
|
||||
}
|
||||
|
||||
class _VipMorePrivilegesPanelState extends State<_VipMorePrivilegesPanel> {
|
||||
static const _collapsedCount = 8; // 收起时展示 2 行 × 4
|
||||
static const _radius = 24.0;
|
||||
bool _expanded = true;
|
||||
|
||||
/// 描边:上→下 #FFFBE5 30% → 0% → 30%(上下可见,左右中段淡出)
|
||||
static const _borderGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0x4DFFFBE5), Color(0x00FFFBE5), Color(0x4DFFFBE5)],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canToggle = widget.list.length > _collapsedCount;
|
||||
final showCount =
|
||||
(!_expanded && canToggle) ? _collapsedCount : widget.list.length;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomPaint(
|
||||
painter: _FigmaGradientBorderPainter(
|
||||
gradient: _borderGradient,
|
||||
strokeWidth: 1,
|
||||
radius: _radius,
|
||||
inner: true,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
child: ColoredBox(
|
||||
color: const Color(0x0DFFFFFF),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 14, 12, 10),
|
||||
child: Column(
|
||||
children: [
|
||||
widget.title,
|
||||
14.sizeBoxH,
|
||||
GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 72 / 110,
|
||||
),
|
||||
itemCount: showCount,
|
||||
itemBuilder: (_, index) =>
|
||||
VipProductPrivilegeItem(widget.list[index]),
|
||||
),
|
||||
if (canToggle) ...[
|
||||
8.sizeBoxH,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_expanded ? '收起特权' : '展开特权',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
_expanded
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
size: 16,
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Figma 线性渐变描边:shader 按节点 bounds 映射(与 Figma stroke fill 一致)
|
||||
class _FigmaGradientBorderPainter extends CustomPainter {
|
||||
final Gradient gradient;
|
||||
final double strokeWidth;
|
||||
final double radius;
|
||||
final bool inner;
|
||||
|
||||
_FigmaGradientBorderPainter({
|
||||
required this.gradient,
|
||||
required this.strokeWidth,
|
||||
required this.radius,
|
||||
this.inner = true,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
// Inner:描边中心线向内缩 strokeWidth/2,使整条描边落在边界内侧
|
||||
final inset = inner ? strokeWidth / 2 : 0.0;
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
rect.deflate(inset),
|
||||
Radius.circular(math.max(0, radius - inset)),
|
||||
);
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..shader = gradient.createShader(rect);
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _FigmaGradientBorderPainter oldDelegate) {
|
||||
return oldDelegate.gradient != gradient ||
|
||||
oldDelegate.strokeWidth != strokeWidth ||
|
||||
oldDelegate.radius != radius ||
|
||||
oldDelegate.inner != inner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../assets_tool/app_colors.dart';
|
||||
import '../../../assets_tool/images.dart';
|
||||
import '../../../config/address.dart';
|
||||
import '../../../hj_model/mine/exchange/dc_model.dart';
|
||||
import '../../../hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import '../../../hj_utils/api_service/mine_service.dart';
|
||||
import '../../../hj_utils/pay/pay_manager.dart';
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
import '../../../tools_base/loading/loading_helper.dart';
|
||||
import '../../../tools_base/net/net_manager.dart';
|
||||
import '../../main_page/provider/msg_provider.dart';
|
||||
import '../../web_page/h5_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'pay_success_alert.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 在线支付弹窗:VIP 会员卡购买(vipProductModel) 或 金币充值(coinRcModel) 二选一,
|
||||
/// 底部弹出,展示支付方式列表 + 支付按钮;下单渠道分代充(dc)/线上跳链/金币三种。
|
||||
class OnlinePayPage extends StatefulWidget {
|
||||
final VipProductModel? vipProductModel; // 传此值:购买 VIP 会员卡
|
||||
final RechargeTypeModel? coinRcModel; // 传此值:充值金币
|
||||
/// 下单来源/实验/会话等埋点信息(透传 /mine/topay)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
const OnlinePayPage({
|
||||
super.key,
|
||||
this.vipProductModel,
|
||||
this.coinRcModel,
|
||||
this.orderTrack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OnlinePayPage> createState() => _OnlinePayPageState();
|
||||
}
|
||||
|
||||
class _OnlinePayPageState extends State<OnlinePayPage> {
|
||||
// 当前选中的支付方式下标
|
||||
int payIndex = 0;
|
||||
// 支付请求防重入标记
|
||||
bool isPaying = false;
|
||||
|
||||
// 加赠券:isCouponPanel 控制切到选券面板,coupon 为已选券
|
||||
bool isCouponPanel = false;
|
||||
CouponModel? coupon;
|
||||
|
||||
// 支付方式列表。会员卡与金币档位二选一,getter 每次访问都会重建列表,故每帧只取一次
|
||||
List<RchgType> get _payTypes =>
|
||||
widget.vipProductModel?.rchgTypeUI ??
|
||||
widget.coinRcModel!.rechargeTypeListUI;
|
||||
|
||||
/// 支付按钮文案:预售(尾款/升级/预订) → 会员卡(升级/普通) → 金币充值
|
||||
String get _payText {
|
||||
final vip = widget.vipProductModel;
|
||||
if (vip == null) return '¥${widget.coinRcModel?.moneyYuan ?? 0}/立即支付';
|
||||
if (!vip.isPreSale)
|
||||
return '¥${vip.discountedPriceUI}/${vip.isUpgrade == true ? '补差价升级' : '立即支付'}';
|
||||
// 预售:可付尾款优先,其次升级,最后预订
|
||||
final preSale = PreSaleProvider();
|
||||
final detail = preSale.preSaleModel?.detailModel;
|
||||
if (preSale.canPayBalance) return '¥${detail?.balanceAmount ?? 0}支付尾款';
|
||||
if (vip.isUpgrade == true) return '¥${(vip.advanceAmount ?? 0) ~/ 10}/立即升级';
|
||||
return '¥${detail?.advanceAmount ?? 0}立即预订';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
||||
decoration: ShapeDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(12.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 选中券类型支付方式时切到选券面板(本项目通常无券数据)
|
||||
child: isCouponPanel
|
||||
? ChoseCouponView(
|
||||
onSelect: (model) => setState(() {
|
||||
isCouponPanel = false;
|
||||
coupon = model;
|
||||
}),
|
||||
)
|
||||
: _payPanel(),
|
||||
);
|
||||
}
|
||||
|
||||
//支付主面板
|
||||
Widget _payPanel() {
|
||||
final types = _payTypes;
|
||||
final curType = types[payIndex]; // 当前选中的支付方式,与列表同源,build 时取
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const SheetHandleBar(),
|
||||
18.sizeBoxH,
|
||||
const Text(
|
||||
'选择支付方式',
|
||||
style: TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
//支付方式列表
|
||||
Wrap(children: [
|
||||
for (var i = 0; i < types.length; i++) _payItem(types[i], i)
|
||||
]),
|
||||
//支付小贴士
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 24.h, left: 16, right: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'支付小贴士:',
|
||||
style: textStyle(
|
||||
14, Colors.white.withValues(alpha: .9), FontWeight.w500),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Text(
|
||||
'''1.因超时支付无法到账,请重新发起。
|
||||
2.每天发起支付不能超过5次,连续发起且未支付,账号可能被加入黑名单。''',
|
||||
style: textStyle(
|
||||
12, Colors.white.withValues(alpha: .45), FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
//支付按钮
|
||||
GestureDetector(
|
||||
onTap: () => _onPay(curType),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 44,
|
||||
decoration: ShapeDecoration(
|
||||
color: AppColors.actionRed,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_payText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20.sp,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
//客服入口
|
||||
Center(
|
||||
child: EasyRichText(
|
||||
'支付中如有问题,请咨询 在线客服',
|
||||
defaultStyle: textStyle(
|
||||
12, Colors.white.withValues(alpha: .6), FontWeight.w500),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '在线客服',
|
||||
style: textStyle(12, Color(0xffFFD460), FontWeight.w500),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => pushToCustomService(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
//单个支付方式
|
||||
Widget _payItem(RchgType type, int index) {
|
||||
final icon = type.getPayIcon();
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => setState(() {
|
||||
//券类型切到选券面板,其余直接切换选中
|
||||
if (type.type == 'coupon') {
|
||||
isCouponPanel = true;
|
||||
} else {
|
||||
payIndex = index;
|
||||
}
|
||||
}),
|
||||
child: Container(
|
||||
height: 41,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
margin: EdgeInsets.only(bottom: 20.h),
|
||||
child: Row(
|
||||
children: [
|
||||
icon.isEmpty
|
||||
? const SizedBox(width: 36, height: 36)
|
||||
: Image.asset(icon, width: 36),
|
||||
13.sizeBoxW,
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${type.typeName}",
|
||||
style: textStyle(
|
||||
16, Colors.white.withValues(alpha: .9), FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
payIndex == index
|
||||
? 'radio_sel.png'.commonImgPath
|
||||
: 'mine_withdraw_nor.png'.mineImgPath,
|
||||
width: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//按选中的支付方式下单:金币余额 / 代充 / 线上跳链
|
||||
void _onPay(RchgType type) {
|
||||
final vip = widget.vipProductModel;
|
||||
final coinRc = widget.coinRcModel;
|
||||
if (type.type == "coin") {
|
||||
_payByCoin();
|
||||
} else if (type.isOfficial == true) {
|
||||
// 正常购买:金币 / vip购买
|
||||
if (coinRc != null) {
|
||||
_payByDc(false, type, coinRc.daichong, coinRc.money);
|
||||
} else {
|
||||
_payByDc(true, type, vip?.daichong, (vip?.discountedPrice ?? 0) * 10);
|
||||
}
|
||||
} else {
|
||||
_payByLink(type);
|
||||
}
|
||||
}
|
||||
|
||||
///线上跳链支付:创建支付单后跳外部支付页
|
||||
Future<void> _payByLink(RchgType payType) async {
|
||||
final vip = widget.vipProductModel; // 非空 = 购买会员卡,空 = 购买金币
|
||||
final productId = vip != null ? vip.productID! : widget.coinRcModel!.id;
|
||||
if (isPaying) return;
|
||||
isPaying = true;
|
||||
LoadingHelper.showLoading();
|
||||
// 合并入口透传;仅实验 ACTIVE 时附带卡皮实验信息(copyWith 无法清 null,故重建)
|
||||
final base = widget.orderTrack ?? const PayOrderTrackInfo();
|
||||
final orderTrack = PayOrderTrackInfo(
|
||||
sourcePage: base.sourcePage ?? PaySourcePage.unknown,
|
||||
sourceRef: base.sourceRef,
|
||||
videoId: base.videoId,
|
||||
activityId: base.activityId,
|
||||
sessionId: base.sessionId,
|
||||
//短剧付费墙的归因字段,这里重建时漏抄会被服务端 8001 拒单
|
||||
mediaId: base.mediaId,
|
||||
contentId: base.contentId,
|
||||
checkoutContextId: base.checkoutContextId,
|
||||
experimentId: vipProductManager.isExperimentActive
|
||||
? (base.experimentId ??
|
||||
(vip == null
|
||||
? null
|
||||
: vip.experimentId ?? vipProductManager.experimentId))
|
||||
: null,
|
||||
experimentVariant: vipProductManager.isExperimentActive
|
||||
? (base.experimentVariant ??
|
||||
(vip == null ? null : vip.variant ?? vipProductManager.variant))
|
||||
: null,
|
||||
);
|
||||
final urlModel = await MineService.chargeGoldCoin(
|
||||
payType.type,
|
||||
productId: productId,
|
||||
isVip: vip != null,
|
||||
goldExtraID: coupon?.cId,
|
||||
finalPayStatus: vip?.isPreSale == true
|
||||
? PreSaleProvider().preSaleModel?.detailModel?.balancePayment
|
||||
: null,
|
||||
orderTrack: orderTrack,
|
||||
);
|
||||
LoadingHelper.dismissLoading();
|
||||
if (urlModel != null) {
|
||||
if (urlModel.mode == "url") {
|
||||
await launchUrlToWeb(urlModel.payUrl ?? '');
|
||||
} else if (urlModel.mode == "sdk") {
|
||||
showToast("没有找到支付类型为:${payType.type} 的sdk");
|
||||
Get.back();
|
||||
isPaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// VIP 已创建支付单:视为产生购买行为,关闭会员卡页不再报无购买关闭
|
||||
if (vip != null) VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
Get.back();
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
//支付成功刷新vip到期时间
|
||||
Get.dialog(
|
||||
const Center(child: PaySuccessAlert()),
|
||||
barrierColor: const Color(0x22000000),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
isPaying = false;
|
||||
}
|
||||
|
||||
///金币余额支付(仅会员卡有该方式)
|
||||
Future<void> _payByCoin() async {
|
||||
if (isPaying) return;
|
||||
isPaying = true;
|
||||
final vip = widget.vipProductModel;
|
||||
final track = widget.orderTrack;
|
||||
final experimentActive = vipProductManager.isExperimentActive;
|
||||
await PayManager().buyVip(
|
||||
vip?.productType,
|
||||
vip?.productID,
|
||||
vip?.productName,
|
||||
vip?.discountedPrice,
|
||||
source: 'online_pay',
|
||||
jumpWalletOnInsufficient: false, // 当前就在充值页,余额不足不跳转
|
||||
finalPayStatus: vip?.isPreSale == true
|
||||
? PreSaleProvider().preSaleModel?.detailModel?.balancePayment
|
||||
: null,
|
||||
// 与 /mine/topay 一致:仅实验 ACTIVE 时回传;sessionId 有则带上
|
||||
experimentId: experimentActive
|
||||
? (track?.experimentId ??
|
||||
vip?.experimentId ??
|
||||
vipProductManager.experimentId)
|
||||
: null,
|
||||
experimentVariant: experimentActive
|
||||
? (track?.experimentVariant ??
|
||||
vip?.variant ??
|
||||
vipProductManager.variant)
|
||||
: null,
|
||||
sessionId: track?.sessionId,
|
||||
//短剧付费墙开卡:金币余额支付这条也要带归因,否则只有第三方充值那条统计得到
|
||||
mediaId: track?.mediaId,
|
||||
contentId: track?.contentId,
|
||||
checkoutContextId: track?.checkoutContextId,
|
||||
onSuccess: (data) async {
|
||||
VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
showToast("购买成功");
|
||||
globalStore.refreshWallet();
|
||||
await globalStore.updateUserInfo();
|
||||
MineMsgProvider().refreshPayPopup(); //会员状态变了,重拉分层与付费引导开关(买完别再弹引导)
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
isPaying = false;
|
||||
}
|
||||
|
||||
///代充支付:拼装代充参数后跳 H5 收银台
|
||||
Future<void> _payByDc(
|
||||
bool isVip, RchgType payType, DCModel? dcModel, int? money) async {
|
||||
// daichong 是所有会员卡/金币档位共用的同一实例,下面的改写只为拼给 H5,必须在副本上做:
|
||||
// 原地改会把 payInfos 截成单条(支付方式列表变短→下标越界)、ordUrl 被重复拼 host
|
||||
final dc = dcModel!.clone();
|
||||
final payList = dc.traders![0].payInfos!;
|
||||
PayInfoModel? payInfo;
|
||||
for (final model in payList) {
|
||||
if (model.payMethod == payType.payMethod) {
|
||||
payInfo = model;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//设置默认值
|
||||
if (dc.limit == 0) dc.limit = 500;
|
||||
final payMoney = money ?? 0;
|
||||
//超额度走大额通道
|
||||
if (payInfo!.payType!.contains(2) && payInfo.payType!.contains(3)) {
|
||||
payInfo.payType = (payMoney / 100) > dc.limit! ? [3] : [2];
|
||||
}
|
||||
if (payInfo.payType!.length >= 3) {
|
||||
payInfo.payType = (payMoney / 100) > dc.limit! ? [1, 3] : [1, 2];
|
||||
}
|
||||
dc.traders![0].payInfos = [payInfo];
|
||||
|
||||
final host = Address.baseHost!;
|
||||
dc.ordUrl = host + dc.ordUrl!;
|
||||
dc.traderUrl = host + dc.traderUrl!;
|
||||
dc.chargeMoney = payMoney ~/ 100;
|
||||
final channel = payType.channel!;
|
||||
dc.channel = channel;
|
||||
final token = await netManager.getToken();
|
||||
//bt64
|
||||
final data = base64Encode(utf8.encode(json.encode(dc)));
|
||||
if (isVip) VipCardAnalyticsSession.markPurchaseOnActiveSessions();
|
||||
Get.to(H5Page(title: "代理充值", url: "$channel/?data=$data&token=$token"),
|
||||
opaque: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加赠券选择面板:拉取用户券列表,选中经 onSelect 回传(传 null 表示返回不选)
|
||||
class ChoseCouponView extends StatefulWidget {
|
||||
final Function(CouponModel? coupon) onSelect;
|
||||
|
||||
const ChoseCouponView({super.key, required this.onSelect});
|
||||
|
||||
@override
|
||||
State<ChoseCouponView> createState() => _ChoseCouponViewState();
|
||||
}
|
||||
|
||||
class _ChoseCouponViewState extends State<ChoseCouponView> {
|
||||
bool isLoading = true;
|
||||
final _dataSource = <CouponModel>[];
|
||||
int page = 1;
|
||||
|
||||
// refreshCtr 由 CustomRefreshView 创建并 dispose,本类只持引用,绝不能再 dispose
|
||||
RefreshController? refreshCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fetch());
|
||||
}
|
||||
|
||||
Future<void> _fetch({bool isRefresh = true}) async {
|
||||
if (isRefresh) page = 1;
|
||||
final res = await MineService.fetchUserCoupons(1, page: page);
|
||||
isLoading = false;
|
||||
|
||||
if (isRefresh) {
|
||||
refreshCtr?.refreshCompleted();
|
||||
_dataSource.clear();
|
||||
}
|
||||
_dataSource.length < 20
|
||||
? refreshCtr?.loadNoData()
|
||||
: refreshCtr?.loadComplete();
|
||||
_dataSource.addAll(res);
|
||||
page += 1;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: Column(
|
||||
children: [
|
||||
//顶部拖动条
|
||||
Container(
|
||||
height: 41,
|
||||
alignment: Alignment.center,
|
||||
child: const SheetHandleBar(color: Colors.black12),
|
||||
),
|
||||
//返回 + 标题
|
||||
Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => widget.onSelect(null),
|
||||
child: Image.asset(
|
||||
'common_back.png'.commonImgPath,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Colors.black.withValues(alpha: .9),
|
||||
),
|
||||
),
|
||||
const Center(
|
||||
child: Text(
|
||||
'选择加赠券',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onRefresh: (_) => _fetch(),
|
||||
onLoading: (_) => _fetch(isRefresh: false),
|
||||
child: () {
|
||||
if (isLoading) return const LoadingCenterWidget();
|
||||
if (_dataSource.isEmpty) return const CErrorWidget();
|
||||
return ListView.separated(
|
||||
separatorBuilder: (_, __) => 12.sizeBoxH,
|
||||
itemCount: _dataSource.length,
|
||||
itemBuilder: (_, index) => _couponItem(_dataSource[index]),
|
||||
);
|
||||
}(),
|
||||
onInit: (ctr) => refreshCtr = ctr),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _couponItem(CouponModel model) {
|
||||
return Container(
|
||||
height: 72,
|
||||
padding: const EdgeInsets.only(left: 24, right: 11),
|
||||
alignment: Alignment.centerLeft,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff891c).withValues(alpha: .4),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
child: Row(
|
||||
children: [
|
||||
//券面额
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'coin_icon.webp'.mineImgPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
4.sizeBoxW,
|
||||
Text(
|
||||
'${model.price ?? 0}',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 33 / 24),
|
||||
)
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'充值加送金币',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: .6), fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
23.sizeBoxW,
|
||||
SizedBox(
|
||||
width: 1,
|
||||
height: 45,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: .5),
|
||||
Colors.black.withValues(alpha: 0)
|
||||
], begin: Alignment.topCenter, end: Alignment.bottomCenter),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
//券说明 + 有效期
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'金币加购券',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'有效期:${model.expireTime?.utcToYMDHM()}',
|
||||
style: TextStyle(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
fontSize: 8,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
11.sizeBoxW,
|
||||
GestureDetector(
|
||||
onTap: () => widget.onSelect(model),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffF68216),
|
||||
borderRadius: BorderRadius.circular(22)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||
child: Text(
|
||||
'立即使用',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/// 创建订单(/mine/topay)的来源页,[value] 与后端约定一致
|
||||
enum PaySourcePage {
|
||||
homeUserSegment('HOME_USER_SEGMENT'), //用户分层首页弹窗
|
||||
homeFloatWindow('HOME_FLOAT_WINDOW'), //首页右下角浮窗(分层浮窗 + 活动浮窗内链)
|
||||
videoBottomBanner('VIDEO_BOTTOM_BANNER'), //视频播放页下方 banner
|
||||
videoBottomSheet('VIDEO_BOTTOM_SHEET'), //视频播放页底部弹窗(含播放页金币解锁)
|
||||
vipCenter('VIP_CENTER'), //会员中心
|
||||
h5Activity('H5_ACTIVITY'), //H5 活动
|
||||
dramaPaywall('DRAMA_PAYWALL'), //短剧付费墙(金币充值 / 开短剧卡)
|
||||
unknown('UNKNOWN'); //入口没指定
|
||||
|
||||
final String value;
|
||||
const PaySourcePage(this.value);
|
||||
}
|
||||
|
||||
/// 下单(/mine/topay)透传的埋点信息:来源 / 实验 / 会话等,均为非必填
|
||||
class PayOrderTrackInfo {
|
||||
/// 来源页
|
||||
final PaySourcePage? sourcePage;
|
||||
|
||||
/// 来源关联(如 videoId 文案标识)
|
||||
final String? sourceRef;
|
||||
|
||||
/// 关联视频 ID
|
||||
final String? videoId;
|
||||
|
||||
/// 活动 ID
|
||||
final String? activityId;
|
||||
|
||||
/// 实验 ID(VIP 卡皮 A/B)
|
||||
final String? experimentId;
|
||||
|
||||
/// 实验分组(对应接口 experimentVariant)
|
||||
final String? experimentVariant;
|
||||
|
||||
/// 会话 ID(与 VIP 卡片统计 session 对齐)
|
||||
final String? sessionId;
|
||||
|
||||
/// 短剧:当前剧 ID
|
||||
final String? mediaId;
|
||||
|
||||
/// 短剧:当前分集 ID
|
||||
final String? contentId;
|
||||
|
||||
/// 短剧:本次付费墙上下文,服务端据此把充值/开卡订单归因到这一集
|
||||
final String? checkoutContextId;
|
||||
|
||||
const PayOrderTrackInfo({
|
||||
this.sourcePage,
|
||||
this.sourceRef,
|
||||
this.videoId,
|
||||
this.activityId,
|
||||
this.experimentId,
|
||||
this.experimentVariant,
|
||||
this.sessionId,
|
||||
this.mediaId,
|
||||
this.contentId,
|
||||
this.checkoutContextId,
|
||||
});
|
||||
|
||||
PayOrderTrackInfo copyWith({
|
||||
PaySourcePage? sourcePage,
|
||||
String? sourceRef,
|
||||
String? videoId,
|
||||
String? activityId,
|
||||
String? experimentId,
|
||||
String? experimentVariant,
|
||||
String? sessionId,
|
||||
String? mediaId,
|
||||
String? contentId,
|
||||
String? checkoutContextId,
|
||||
}) {
|
||||
return PayOrderTrackInfo(
|
||||
sourcePage: sourcePage ?? this.sourcePage,
|
||||
sourceRef: sourceRef ?? this.sourceRef,
|
||||
videoId: videoId ?? this.videoId,
|
||||
activityId: activityId ?? this.activityId,
|
||||
experimentId: experimentId ?? this.experimentId,
|
||||
experimentVariant: experimentVariant ?? this.experimentVariant,
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
mediaId: mediaId ?? this.mediaId,
|
||||
contentId: contentId ?? this.contentId,
|
||||
checkoutContextId: checkoutContextId ?? this.checkoutContextId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_dialog.dart';
|
||||
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
|
||||
class PaySuccessAlert extends StatelessWidget {
|
||||
const PaySuccessAlert({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommonDialog(
|
||||
child: _buildContent(),
|
||||
);
|
||||
}
|
||||
|
||||
_buildContent() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"支付确认",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
0.5.line,
|
||||
12.sizeBoxH,
|
||||
Text(
|
||||
'''1.订单支付后,一般会在1-10分钟内到账,如超时未到账,请联系在线客服为您处理。
|
||||
2.受特殊行业限制,如支付失败可尝试重新发起订单,系统将会随机切换备用的支付通道。
|
||||
3.本APP有稳定的广告收益,产品稳定安全,请放心充值使用,如遇报毒提示忽略即可。''',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55),
|
||||
fontSize: 14,
|
||||
height: 1.8,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToCustomService(),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'支付遇到问题',
|
||||
style: TextStyle(color: Color(0xff999999), fontSize: 14),
|
||||
),
|
||||
2.sizeBoxW,
|
||||
Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
)
|
||||
],
|
||||
)),
|
||||
24.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
"支付成功",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:hgdj/hj_model/mine/vip_card_analytics_event.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_vip/vip_product_manager.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_vip/vip_support_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// VIP 卡片统计:一次进入会员卡页/购买弹窗对应一个 session,关闭时按是否下单上报。
|
||||
/// 曝光在 UI 实际展示后(post-frame)上报。
|
||||
class VipCardAnalyticsSession {
|
||||
VipCardAnalyticsSession() {
|
||||
// 进页时快照实验上下文:关闭时 manager 的原始数据可能已被重拉/失效,不能再读 live manager
|
||||
_experimentActive = vipProductManager.isExperimentActive;
|
||||
_experimentId = vipProductManager.experimentId;
|
||||
_variant = vipProductManager.variant;
|
||||
_active.add(this);
|
||||
}
|
||||
|
||||
static final _uuid = Uuid();
|
||||
static final Set<VipCardAnalyticsSession> _active = {};
|
||||
|
||||
/// 当前访问会话 ID(同一次进入会员卡 UI 内事件共用)
|
||||
final String sessionId = _uuid.v4();
|
||||
|
||||
/// 进页瞬间的实验快照(关闭上报必须用这份,避免中途 force 重拉把 live 态冲掉)
|
||||
late final bool _experimentActive;
|
||||
late final String? _experimentId;
|
||||
late final String? _variant;
|
||||
|
||||
bool _pageViewReported = false;
|
||||
bool _purchased = false;
|
||||
bool _closed = false;
|
||||
String? _lastImpressedProductId;
|
||||
|
||||
/// 同一帧内 jumpToPage + onPageChanged 去重
|
||||
String? _pendingImpressionProductId;
|
||||
|
||||
static String _nowUtc() =>
|
||||
DateTimeUtil.format2utc(DateTime.now().toUtc()) ?? '';
|
||||
|
||||
/// DISABLED / 无实验 ID:不上报(空 experimentId 会参数错误)
|
||||
bool get _canReport {
|
||||
if (!_experimentActive) return false;
|
||||
final id = _experimentId;
|
||||
return id != null && id.isNotEmpty;
|
||||
}
|
||||
|
||||
VipCardAnalyticsEvent _build({
|
||||
required String eventName,
|
||||
String? productId,
|
||||
int? price,
|
||||
}) {
|
||||
return VipCardAnalyticsEvent(
|
||||
eventId: _uuid.v4(),
|
||||
eventName: eventName,
|
||||
sessionId: sessionId,
|
||||
occurredAt: _nowUtc(),
|
||||
experimentId: _experimentId,
|
||||
variant: _variant,
|
||||
productId: productId,
|
||||
price: price,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _send(List<VipCardAnalyticsEvent> events) async {
|
||||
if (!_canReport || events.isEmpty) return;
|
||||
try {
|
||||
await MineService.reportAnalyticsEvents(events);
|
||||
} catch (e) {
|
||||
debugLog('VipCardAnalytics', e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 卡皮页展示:UI 就绪后只报一次
|
||||
void reportPageViewAfterPaint() {
|
||||
if (_pageViewReported || _closed || !_canReport) return;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pageViewReported || _closed || !_canReport) return;
|
||||
_pageViewReported = true;
|
||||
_send([_build(eventName: VipCardAnalyticsEventName.pageView)]);
|
||||
});
|
||||
}
|
||||
|
||||
/// 套餐曝光:选中 / 默认选中各算一次;同 product 连续重复不重复报
|
||||
void reportProductImpression(VipProductModel? product,
|
||||
{bool afterPaint = false}) {
|
||||
final productId = product?.productID;
|
||||
if (productId == null || productId.isEmpty || _closed || !_canReport)
|
||||
return;
|
||||
if (_lastImpressedProductId == productId) return;
|
||||
|
||||
void doReport() {
|
||||
if (_closed || !_canReport) return;
|
||||
if (_lastImpressedProductId == productId) return;
|
||||
_lastImpressedProductId = productId;
|
||||
_pendingImpressionProductId = null;
|
||||
_send([
|
||||
_build(
|
||||
eventName: VipCardAnalyticsEventName.productImpression,
|
||||
productId: productId,
|
||||
price: product?.discountedPrice,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (afterPaint) {
|
||||
_pendingImpressionProductId = productId;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (_pendingImpressionProductId != productId) return;
|
||||
doReport();
|
||||
});
|
||||
} else {
|
||||
doReport();
|
||||
}
|
||||
}
|
||||
|
||||
/// 下单成功:本访问内关闭不再报「无购买关闭」
|
||||
void markPurchased() => _purchased = true;
|
||||
|
||||
/// 活跃会话均标记已购买(支付页成功回调用)
|
||||
static void markPurchaseOnActiveSessions() {
|
||||
for (final s in _active) {
|
||||
s.markPurchased();
|
||||
}
|
||||
}
|
||||
|
||||
/// 无购买关闭
|
||||
void reportCloseWithoutPurchaseIfNeeded() {
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
_active.remove(this);
|
||||
if (_purchased) {
|
||||
debugLog(
|
||||
'VipCardAnalytics', 'skip CLOSE_WITHOUT_PURCHASE: already purchased');
|
||||
return;
|
||||
}
|
||||
if (!_canReport) {
|
||||
debugLog(
|
||||
'VipCardAnalytics',
|
||||
'skip CLOSE_WITHOUT_PURCHASE: canReport=false active=$_experimentActive id=$_experimentId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 关闭瞬间触发上报;不 await,避免卡住 onClose,但请求走全局 http 不会随页面 dispose 取消
|
||||
_send([_build(eventName: VipCardAnalyticsEventName.closeWithoutPurchase)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../main_page/provider/msg_provider.dart';
|
||||
import 'vip_product_manager.dart';
|
||||
import 'vip_support_model.dart';
|
||||
import 'vip_ui_kit.dart';
|
||||
|
||||
class VipCardItem extends StatefulWidget {
|
||||
final Function()? callback;
|
||||
final bool isSelect;
|
||||
final VipProductModel model;
|
||||
|
||||
const VipCardItem(this.model,
|
||||
{super.key, this.callback, this.isSelect = false});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _VipCardItemState();
|
||||
}
|
||||
}
|
||||
|
||||
class _VipCardItemState extends State<VipCardItem> {
|
||||
/// 分层倒计时停表后(如已是会员),新人卡仍需本地秒级刷新
|
||||
final ValueNotifier<int> _localTick = ValueNotifier<int>(0);
|
||||
Timer? _localTimer;
|
||||
|
||||
/// 仅按套餐 badgeType 决定角标,不看名称 / sort / 默认选中 / actionDesc
|
||||
String get _badgeType => (widget.model.badgeType ?? '').trim().toUpperCase();
|
||||
|
||||
String? get _badgeLabel {
|
||||
switch (_badgeType) {
|
||||
case 'MOST_POPULAR':
|
||||
case 'NEW_USER_OFFER':
|
||||
final text = widget.model.badgeText?.trim() ?? '';
|
||||
if (text.isNotEmpty) return text;
|
||||
// NEW_USER_OFFER 无文案时兜底
|
||||
return _badgeType == 'NEW_USER_OFFER' ? '新人特惠' : null;
|
||||
default:
|
||||
// 角标只认 badgeType / badgeText,不回退 actionDesc / desc
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isMostPopularBadge => _badgeType == 'MOST_POPULAR';
|
||||
|
||||
/// 仅 productType==5(新人卡)展示倒计时
|
||||
bool get _isNewerOfferCard => widget.model.productType == 5;
|
||||
|
||||
/// 当前应展示的倒计时:仅新人卡;优先分层 lastDiscountTime;会员态无分层时用卡 showCountdownTime(小时) 滚动
|
||||
({String hour, String min, String sec})? get _countdownParts {
|
||||
if (!_isNewerOfferCard) return null;
|
||||
final layered = MineMsgProvider().countdownConfig;
|
||||
if (layered != null) {
|
||||
return (
|
||||
hour: layered.discountHour,
|
||||
min: layered.discountMin,
|
||||
sec: layered.discountSec
|
||||
);
|
||||
}
|
||||
final hours = widget.model.showCountdownTime ?? 0;
|
||||
if (hours <= 0) return null;
|
||||
final period = hours * 3600;
|
||||
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
var remain = period - (nowSec % period);
|
||||
if (remain <= 0) remain = period;
|
||||
final h = (remain ~/ 3600).toString().padLeft(2, '0');
|
||||
final m = ((remain ~/ 60) % 60).toString().padLeft(2, '0');
|
||||
final s = (remain % 60).toString().padLeft(2, '0');
|
||||
return (hour: h, min: m, sec: s);
|
||||
}
|
||||
|
||||
bool get _needLocalTick =>
|
||||
_isNewerOfferCard &&
|
||||
MineMsgProvider().countdownConfig == null &&
|
||||
(widget.model.showCountdownTime ?? 0) > 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncLocalTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VipCardItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_syncLocalTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_localTimer?.cancel();
|
||||
_localTick.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _syncLocalTimer() {
|
||||
if (_needLocalTick) {
|
||||
_localTimer ??= Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
_localTick.value++;
|
||||
});
|
||||
} else {
|
||||
_localTimer?.cancel();
|
||||
_localTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
String get _title => (widget.model.newName?.trim().isNotEmpty == true)
|
||||
? widget.model.newName!.trim()
|
||||
: (widget.model.productName ?? '');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_syncLocalTimer();
|
||||
// variant=B:组合卡 UI;A / DISABLED:接口卡图
|
||||
if (vipProductManager.isNewVipUi) {
|
||||
return _buildVariantBCard();
|
||||
}
|
||||
return _buildClassicImageCard();
|
||||
}
|
||||
|
||||
/// 旧版:选中/未选中靠接口下发卡面图 + 角标/新人倒计时叠加
|
||||
Widget _buildClassicImageCard() {
|
||||
final badgeLabel = _badgeLabel;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: widget.callback,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 9,
|
||||
bottom: 10,
|
||||
right: 0,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: widget.isSelect
|
||||
? (widget.model.realSelectVipImage().isEmpty
|
||||
? widget.model.realNormalVipImage()
|
||||
: widget.model.realSelectVipImage())
|
||||
: widget.model.realNormalVipImage(),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
if (badgeLabel != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildBadge(badgeLabel),
|
||||
),
|
||||
if (_isNewerOfferCard)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: _buildNewerTimer(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// variant=B:148×157 组合卡;选中金渐变+#B2FFF6DC 边;未选中 #211C12+#14FFFFFF 边
|
||||
Widget _buildVariantBCard() {
|
||||
final selected = widget.isSelect;
|
||||
final badgeLabel = _badgeLabel;
|
||||
const cardW = 148.0;
|
||||
const cardH = 157.0;
|
||||
const badgeTop = 8.0;
|
||||
// VIP/Glow Gold:Y10+Blur28,底部预留绘制空间,避免被轮播裁切
|
||||
const glowPadBottom = 24.0;
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: widget.callback,
|
||||
child: SizedBox(
|
||||
width: cardW,
|
||||
height: cardH + badgeTop + glowPadBottom,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: badgeTop,
|
||||
height: cardH,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
boxShadow: selected
|
||||
? const [
|
||||
// VIP/Glow Gold:Y10 / Blur28 / Spread-6 / #DEAB54 35%
|
||||
BoxShadow(
|
||||
color: Color(0x59DEAB54),
|
||||
offset: Offset(0, 10),
|
||||
blurRadius: 28,
|
||||
spreadRadius: -6,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 底色 / 渐变
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? null : const Color(0xff211C12),
|
||||
gradient: selected
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xffFBF0CD),
|
||||
Color(0xffE5B75D)
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
// 内容(含底栏 desc)
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: _buildBCardBody(selected)),
|
||||
_buildBCardFooter(selected),
|
||||
],
|
||||
),
|
||||
// 边框置顶,避免被底栏遮住;新人卡选中不描边
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: (selected && _isNewerOfferCard)
|
||||
? null
|
||||
: Border.all(
|
||||
color: selected
|
||||
? const Color(0xB2FFF6DC)
|
||||
: const Color(0x14FFFFFF),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (badgeLabel != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: _buildBadge(badgeLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBCardBody(bool selected) {
|
||||
final titleColor = selected ? const Color(0xff3B2B0E) : Color(0xffF6EEDC);
|
||||
final priceColor =
|
||||
selected ? const Color(0xff3B2B0E) : const Color(0xffF6EEDC);
|
||||
final originColor =
|
||||
selected ? const Color(0xff6C644F) : const Color(0xff9A927C);
|
||||
final showOrigin = widget.model.originalPriceUI > 0 &&
|
||||
widget.model.originalPriceUI != widget.model.discountedPriceUI;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 22, 10, 8),
|
||||
child: Column(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: titleColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
EasyRichText(
|
||||
'¥${widget.model.discountedPriceUI}',
|
||||
defaultStyle: TextStyle(
|
||||
color: priceColor,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '¥',
|
||||
matchWordBoundaries: false,
|
||||
matchOption: 'first',
|
||||
style: TextStyle(
|
||||
color: priceColor,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showOrigin) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'¥${widget.model.originalPriceUI}',
|
||||
style: TextStyle(
|
||||
color: originColor,
|
||||
fontSize: 13,
|
||||
decoration: TextDecoration.lineThrough,
|
||||
decorationColor: originColor,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// 无原价时占位短横,对齐设计稿
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
'-',
|
||||
style: TextStyle(color: originColor, fontSize: 13, height: 1.2),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBCardFooter(bool selected) {
|
||||
if (!_isNewerOfferCard) return _buildBDurationFooter(selected);
|
||||
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _needLocalTick ? _localTick : MineMsgProvider().tick,
|
||||
builder: (_, __, ___) {
|
||||
final cd = _countdownParts;
|
||||
if (cd != null) {
|
||||
if (selected) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
alignment: Alignment.center,
|
||||
color: const Color(0xffC14A38),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_timerBox(cd.hour),
|
||||
_timerColon(),
|
||||
_timerBox(cd.min),
|
||||
_timerColon(),
|
||||
_timerBox(cd.sec),
|
||||
4.sizeBoxW,
|
||||
const Text(
|
||||
'结束',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
alignment: Alignment.center,
|
||||
color: const Color(0xff16140F),
|
||||
child: Text(
|
||||
'${cd.hour}:${cd.min}:${cd.sec}后失效',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff8A8A8A),
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _buildBDurationFooter(selected);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 无倒计时时的底栏:取套餐 desc
|
||||
Widget _buildBDurationFooter(bool selected) {
|
||||
final text = widget.model.desc?.trim() ?? '';
|
||||
if (text.isEmpty) return const SizedBox.shrink();
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
alignment: Alignment.center,
|
||||
color: selected ? const Color(0xff2B2314) : const Color(0xff1A150C),
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: selected ? const Color(0xffFFFFFF) : const Color(0xff9A927C),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(String label) {
|
||||
final isMostPopular = _isMostPopularBadge;
|
||||
final useB = vipProductManager.isNewVipUi;
|
||||
// B 未选中:统一深底金边/金字;B 选中 / A:优先 uiConfig.badgeStyles
|
||||
final useApiStyle = useB ? widget.isSelect : true;
|
||||
Color? apiBg;
|
||||
Color? apiFg;
|
||||
if (useApiStyle) {
|
||||
final style = vipProductManager.badgeStyleFor(_badgeType);
|
||||
apiBg = _parseHexColor(style?.backgroundColor);
|
||||
apiFg = _parseHexColor(style?.textColor);
|
||||
}
|
||||
final Color bg;
|
||||
final Color fg;
|
||||
final Border? border;
|
||||
if (useB && !widget.isSelect) {
|
||||
bg = const Color(0xCC1A1A1A);
|
||||
fg = const Color(0xFFE8C078);
|
||||
border = Border.all(color: const Color(0xFFE8C078), width: 0.8);
|
||||
} else {
|
||||
bg = apiBg ??
|
||||
(isMostPopular ? const Color(0xCC1A1A1A) : const Color(0xffE1351F));
|
||||
fg = apiFg ??
|
||||
(isMostPopular ? const Color(0xFFE8C078) : const Color(0xE5FFFFFF));
|
||||
border = (apiBg == null && isMostPopular)
|
||||
? Border.all(color: const Color(0xFFE8C078), width: 0.8)
|
||||
: null;
|
||||
}
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 111),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: bg,
|
||||
border: border,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析 #RRGGBB / RRGGBB / #AARRGGBB;非法或空返回 null
|
||||
Color? _parseHexColor(String? raw) {
|
||||
var hex = (raw ?? '').trim().toUpperCase().replaceAll('#', '');
|
||||
if (hex.isEmpty) return null;
|
||||
if (hex.length == 6) hex = 'FF$hex';
|
||||
if (hex.length != 8) return null;
|
||||
final value = int.tryParse(hex, radix: 16);
|
||||
return value == null ? null : Color(value);
|
||||
}
|
||||
|
||||
/// A / DISABLED 旧版新人倒计时:卡底橙色小标签「HH:MM:SS」
|
||||
Widget _buildNewerTimer() {
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _needLocalTick ? _localTick : MineMsgProvider().tick,
|
||||
builder: (_, __, ___) {
|
||||
final cd = _countdownParts;
|
||||
if (cd == null) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xffF68804),
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
),
|
||||
child: Text(
|
||||
'${cd.hour}:${cd.min}:${cd.sec}',
|
||||
style: const TextStyle(color: Color(0xff000000), fontSize: 10),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timerBox(String value) {
|
||||
return Container(
|
||||
width: 17,
|
||||
height: 16,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _timerColon() => const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Text(':',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600)),
|
||||
);
|
||||
}
|
||||
|
||||
class VipProductPrivilegeItem extends StatelessWidget {
|
||||
final NewPrivilege? model;
|
||||
final bool isShowOnePrivilege;
|
||||
|
||||
/// true:旧版白字圆图标样式(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const VipProductPrivilegeItem(
|
||||
this.model, {
|
||||
super.key,
|
||||
this.isShowOnePrivilege = false,
|
||||
this.classic = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) {
|
||||
return Column(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 25,
|
||||
),
|
||||
3.sizeBoxH,
|
||||
Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white.withValues(alpha: .9)),
|
||||
),
|
||||
if (!isShowOnePrivilege) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(0),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(1),
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 9,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
),
|
||||
6.sizeBoxH,
|
||||
VipGradientMask(
|
||||
child: Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w500, color: Colors.white),
|
||||
),
|
||||
),
|
||||
if (!isShowOnePrivilege) ...[
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
_descLine(0),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
_descLine(1),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .45),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 9,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _descLine(int index) {
|
||||
final raw = model?.privilegeDesc ?? '';
|
||||
final bySpace = raw.split(' ');
|
||||
if (bySpace.length > 1) {
|
||||
return index < bySpace.length ? bySpace[index].trim() : '';
|
||||
}
|
||||
final byLine = raw.split('\n');
|
||||
return index < byLine.length ? byLine[index].trim() : '';
|
||||
}
|
||||
}
|
||||
|
||||
/// 核心权益卡片
|
||||
/// - classic=false:左图标 + 右标题/描述(会员中心 B / 购买弹窗)
|
||||
/// - classic=true:72×72 金边方卡(会员中心 A / DISABLED)
|
||||
class VipCorePrivilegeCard extends StatelessWidget {
|
||||
final NewPrivilege? model;
|
||||
final bool classic;
|
||||
|
||||
const VipCorePrivilegeCard(this.model, {super.key, this.classic = false});
|
||||
|
||||
static const _radius = 16.0;
|
||||
|
||||
static const _fillGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x12FFFFFF), // #FFFFFF 7.06%
|
||||
Color(0x05FFFFFF), // #FFFFFF 7.06%
|
||||
],
|
||||
);
|
||||
|
||||
static const _borderGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0x29FFFFFF), // #FFFFFF 16.08%
|
||||
Color(0x0AFFFFFF), // #FFFFFF 3.92%
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) return _buildClassic();
|
||||
return Container(
|
||||
width: 168,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x1AFFFFFF),
|
||||
offset: Offset(0, 1),
|
||||
blurRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Color(0x4D000000),
|
||||
offset: Offset(0, 8),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -8,
|
||||
),
|
||||
],
|
||||
),
|
||||
// 渐变描边用 CustomPaint 画 stroke,避免双层 Container「假边框」透出底层背景
|
||||
child: CustomPaint(
|
||||
painter: _GradientBorderPainter(
|
||||
gradient: _borderGradient,
|
||||
strokeWidth: 1,
|
||||
radius: _radius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_radius),
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(gradient: _fillGradient),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildIcon(),
|
||||
10.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
model?.privilegeName ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF6EEDC),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
_buildDesc(model?.privilegeDesc ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 旧版:正方形金框 + 深棕底图 + 图标/标题/描述
|
||||
Widget _buildClassic() {
|
||||
return Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('core_privilege_bg.webp'.videoPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xffFFF576), width: 0.5),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 0),
|
||||
5.sizeBoxH,
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => const LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xffFF9642), Color(0xffFFE7BD), Color(0xffFCCF36)],
|
||||
stops: [0.066, 0.49, 0.914],
|
||||
).createShader(bounds),
|
||||
blendMode: BlendMode.srcIn,
|
||||
child: Text(
|
||||
model?.privilegeName ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
3.sizeBoxH,
|
||||
Text(
|
||||
model?.privilegeDesc ?? '',
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Color(0xffB7A463), fontSize: 9),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon() {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0x14FFFFFF),
|
||||
border: Border.all(color: const Color(0x66DCAD55), width: 0.8),
|
||||
),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: model?.img ?? '',
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 设计稿描述:前半灰、后半金(支持空格分隔;否则前 4 字灰、其余金)
|
||||
Widget _buildDesc(String raw) {
|
||||
if (raw.isEmpty) return const SizedBox.shrink();
|
||||
return Text(
|
||||
raw,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: Color(0xff9A927C), fontSize: 11, height: 1.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1px 内渐变描边(stroke),不占用布局厚度,避免双层 Container 透底
|
||||
class _GradientBorderPainter extends CustomPainter {
|
||||
final Gradient gradient;
|
||||
final double strokeWidth;
|
||||
final double radius;
|
||||
|
||||
_GradientBorderPainter({
|
||||
required this.gradient,
|
||||
required this.strokeWidth,
|
||||
required this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
final inset = strokeWidth / 2;
|
||||
final rrect = RRect.fromRectAndRadius(
|
||||
rect.deflate(inset),
|
||||
Radius.circular(math.max(0, radius - inset)),
|
||||
);
|
||||
final paint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..shader = gradient.createShader(rect);
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _GradientBorderPainter oldDelegate) {
|
||||
return oldDelegate.gradient != gradient ||
|
||||
oldDelegate.strokeWidth != strokeWidth ||
|
||||
oldDelegate.radius != radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_alert.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
|
||||
import 'mine_charge_vip_logic.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员支付按钮:会员卡页 MineChargeVipPage 与购买弹窗 BuyVipAlert 共用
|
||||
/// [classic] true:旧版红底/升级金橙;false:改版金渐变 CTA
|
||||
class VipPayButton extends StatelessWidget {
|
||||
final MineChargeVipLogic logic;
|
||||
final bool classic;
|
||||
|
||||
const VipPayButton(this.logic, {super.key, this.classic = false});
|
||||
|
||||
static const _ctaGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xffFBEDC2),
|
||||
Color(0xffEFCB84),
|
||||
Color(0xffD9A346),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final product = logic.currentProductModel;
|
||||
//预售首付或非升级预售 → 预售按钮(透传会员中心 sessionId / 实验字段)
|
||||
if (product?.isPreSale == true && product?.isUpgrade == false) {
|
||||
return PreSaleActivityButton(onTap: () async {
|
||||
await PreSaleProvider()
|
||||
.startPay(product!, orderTrack: logic.buildOrderTrack());
|
||||
logic.update();
|
||||
});
|
||||
}
|
||||
if (classic) return _buildClassic(product);
|
||||
return GestureDetector(
|
||||
onTap: logic.onInitiatePayAction,
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x66DEAB54),
|
||||
offset: Offset(0, 10),
|
||||
blurRadius: 30,
|
||||
spreadRadius: -6,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(gradient: _ctaGradient),
|
||||
child: SizedBox.expand(),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
child: IgnorePointer(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
const Color(0x8CFFFFFF),
|
||||
const Color(0x00FFFFFF),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_payText,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff3D2914),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 旧版:升级卡金橙渐变 / 普通卡红底
|
||||
Widget _buildClassic(VipProductModel? product) {
|
||||
final isUpgrade = product?.isUpgrade ?? false;
|
||||
return GestureDetector(
|
||||
onTap: logic.onInitiatePayAction,
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
gradient: isUpgrade
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFFFFAF50), Color(0xFFE75100)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
)
|
||||
: null,
|
||||
color: isUpgrade ? null : AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
_payText,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String get _payText {
|
||||
final product = logic.currentProductModel;
|
||||
if (product == null) return '';
|
||||
if (product.isPreSale) {
|
||||
if (product.isUpgrade ?? false) {
|
||||
return '${(product.advanceAmount ?? 0) ~/ 10}元/立即升级';
|
||||
}
|
||||
if (PreSaleProvider().canPayBalance) {
|
||||
return '¥${PreSaleProvider().preSaleModel?.detailModel?.balanceAmount ?? 0}/支付尾款';
|
||||
}
|
||||
return '¥${PreSaleProvider().preSaleModel?.detailModel?.advanceAmount ?? 0}/立即预订';
|
||||
}
|
||||
if (product.isUpgrade ?? false) {
|
||||
return '¥${product.discountedPriceUI}/补差价升级';
|
||||
}
|
||||
return '¥${product.discountedPriceUI}/立即支付';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_helper.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
|
||||
import '../../../hj_page/main_page/provider/msg_provider.dart';
|
||||
import '../../../hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'online_pay_page.dart';
|
||||
import 'pay_order_source.dart';
|
||||
import 'vip_card_analytics.dart';
|
||||
import 'vip_support_model.dart';
|
||||
|
||||
/// 会员卡 / 短剧卡列表统一数据源(全站唯一):请求 / 缓存 / 组装 / 选卡兜底 / 按卡ID直接拉支付。
|
||||
/// 会员中心页、购买弹窗、分层横幅等「需要卡列表」的地方用 Consumer<VipProductManager> 消费。
|
||||
/// 页面级状态(当前选中卡 / 轮播控制器 / 支付触发)仍留在各自 Logic,避免多入口串味。
|
||||
class VipProductManager with ChangeNotifier {
|
||||
static final VipProductManager _instance = VipProductManager._();
|
||||
factory VipProductManager() => _instance;
|
||||
VipProductManager._();
|
||||
|
||||
/// 按卡 id 取卡,会员卡和短剧卡共用。id 空/匹配不上都给 null,让调用方用 `??` 往下一级兜。
|
||||
/// 空串必须当「未指定」:分层/实验/短剧默认卡没配时下发的都是空串,
|
||||
/// 不拦会跟 productID 为空的脏数据撞上
|
||||
VipProductModel? _pick(List<VipProductModel> list, String? id) =>
|
||||
(id ?? '').isEmpty
|
||||
? null
|
||||
: list.firstWhereOrNull((e) => e.productID == id);
|
||||
|
||||
// ===== 会员卡数据 =====
|
||||
|
||||
/// 原始接口数据(含代充/预售卡组/升级卡等 / 实验态)
|
||||
VipSupportModel? _raw;
|
||||
|
||||
/// 分层变化等场景标记需重拉;不清 _raw,避免已挂载会员页丢失实验态(B 皮闪回 A)
|
||||
bool _needsReload = false;
|
||||
|
||||
/// 组装后的会员卡列表(常规卡 + 符合条件的预售卡,按 sort 排序)——全站共享一份
|
||||
final vipCards = <VipProductModel>[];
|
||||
|
||||
/// 加载态
|
||||
bool isLoading = true;
|
||||
|
||||
/// 预售卡分组(会员中心页据此判断是否显示预售入口)
|
||||
VipProductListModel? presaleGroup;
|
||||
|
||||
// ===== A/B 实验 =====
|
||||
|
||||
/// 实验是否生效(ACTIVE);DISABLED 时不挂实验字段、不上报 analytics、topay 不传实验参数
|
||||
bool get isExperimentActive => _raw?.isExperimentActive == true;
|
||||
|
||||
/// 会员中心 UI:仅 ACTIVE 且 variant=B 用改版;A / DISABLED / 无实验时 variant 恒为 null,自然走旧版
|
||||
bool get isNewVipUi => (variant ?? '').trim().toUpperCase() == 'B';
|
||||
|
||||
/// 实验字段(仅 ACTIVE 时有值,下单时原样回传)
|
||||
String? get experimentId => isExperimentActive ? _raw?.experimentId : null;
|
||||
|
||||
String? get variant => isExperimentActive ? _raw?.variant : null;
|
||||
|
||||
/// 实验指定的默认卡 id。和短剧卡的 [_dramaCardId] 是两码事,别混
|
||||
String? get _abCardId => isExperimentActive ? _raw?.defaultProductId : null;
|
||||
|
||||
/// 皮肤配置:ACTIVE 时才消费;DISABLED / 无实验不走接口皮肤
|
||||
VipUiConfig? get _uiConfig => isExperimentActive ? _raw?.uiConfig : null;
|
||||
|
||||
/// 会员中心背景图:接口给了就用网络图,否则 null(走本地默认)
|
||||
String? get vipBgImage {
|
||||
final url = (_uiConfig?.backgroundImage ?? '').trim();
|
||||
return url.isEmpty ? null : url;
|
||||
}
|
||||
|
||||
/// 角标配色:按套餐 badgeType 匹配皮肤配置
|
||||
VipBadgeStyle? badgeStyleFor(String? badgeType) =>
|
||||
_uiConfig?.styleFor(badgeType);
|
||||
|
||||
// ===== 会员卡:请求 / 组装 =====
|
||||
|
||||
/// 请求会员卡列表。[force] 为 false 且缓存有效时,直接用缓存重组,不发请求。
|
||||
Future<void> loadVipCards({bool force = false}) async {
|
||||
if (_raw != null && !force && !_needsReload) {
|
||||
_rebuild();
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
final result = await MineService.getVipProduct();
|
||||
// 容错:请求失败或返回空数据时,保留上次成功的数据,只复位加载态,不覆盖 _raw/vipCards
|
||||
if (result == null || (result.list?.isEmpty ?? true)) {
|
||||
isLoading = false;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
_raw = result;
|
||||
_needsReload = false;
|
||||
_rebuild();
|
||||
}
|
||||
|
||||
/// 缓存失效:标记下次 load 强制重拉;保留 _raw/vipCards,避免购卡后已挂载页实验皮闪回 A。
|
||||
/// (分层状态变化时调用)
|
||||
void invalidate() {
|
||||
_needsReload = true;
|
||||
}
|
||||
|
||||
/// 切换账号后:清掉上个用户的数据并按新用户权限重新拉取。
|
||||
/// 卡列表(尤其预售卡)由后端按用户权限/购买状态返回,切号必须重拉,否则残留上个账号的卡。
|
||||
Future<void> reloadForUser() async {
|
||||
_raw = null;
|
||||
_needsReload = true;
|
||||
vipCards.clear();
|
||||
await loadVipCards(force: true);
|
||||
}
|
||||
|
||||
/// 用 _raw 组装 vipCards(会员卡组 + 符合条件的预售卡)
|
||||
void _rebuild() {
|
||||
isLoading = false;
|
||||
presaleGroup = _raw?.list?.firstWhereOrNull((e) => e.position == '预售卡');
|
||||
vipCards
|
||||
..clear()
|
||||
..addAll(_assemble(_raw));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 纯组装:会员卡组 + 符合条件的预售卡。无副作用,供 _rebuild 与 payByVipCard 共用。
|
||||
/// 卡序统一按 sort 升序(含卡皮 ACTIVE)
|
||||
List<VipProductModel> _assemble(VipSupportModel? raw) {
|
||||
final cardGroup = raw?.list?.firstWhereOrNull((e) => e.position == '会员卡');
|
||||
final presale = raw?.list?.firstWhereOrNull((e) => e.position == '预售卡');
|
||||
final list = <VipProductModel>[...(cardGroup?.list ?? [])];
|
||||
// 优先添加预售卡(预售活动中且未付全款)
|
||||
if ((presale?.list ?? []).isNotEmpty &&
|
||||
PreSaleProvider().advanceStatus?.activityStatus == true &&
|
||||
!PreSaleProvider().isPayAll) {
|
||||
list.add(presale!.list!.first..isPreSale = true);
|
||||
}
|
||||
list.sort((a, b) => (a.sort ?? 0).compareTo(b.sort ?? 0));
|
||||
return list;
|
||||
}
|
||||
|
||||
// ===== 会员卡:选卡 / 支付 =====
|
||||
|
||||
/// 默认选中卡:外部指定卡ID > 分层卡 > 实验默认卡 > 升级差价卡 > 保留当前 > 第一张。
|
||||
/// 选中态归页面 Logic,这里只按 vipCards 计算,[vipID]/[current] 由调用方透传。
|
||||
VipProductModel? defaultVipCard({String? vipID, VipProductModel? current}) {
|
||||
if (vipCards.isEmpty) return null;
|
||||
return _pick(vipCards, vipID) ??
|
||||
_pick(vipCards, MineMsgProvider().payTier?.config?.vipCard) ??
|
||||
_pick(vipCards, _abCardId) ??
|
||||
(globalStore.meInfo?.isUpgrade == true
|
||||
? _cheapestUpgrade(vipCards)
|
||||
: null) ??
|
||||
_pick(vipCards, current?.productID) ?? // 二次刷新时保留当前选中
|
||||
vipCards.first;
|
||||
}
|
||||
|
||||
/// 按卡 ID 直接拉起支付弹窗(跳过会员中心列表页)。无数据时先请求一次。
|
||||
/// 选卡:传入 [cardId] > 分层卡 > 实验默认卡 > 升级差价卡 > 第一张
|
||||
/// [reportAnalytics] 是否上报 VIP 卡皮事件(PAGE_VIEW / 曝光 / CLOSE);视频底部分层 banner 等入口传 false
|
||||
Future<void> payByVipCard(
|
||||
String? cardId, {
|
||||
PayOrderTrackInfo? orderTrack,
|
||||
bool reportAnalytics = true,
|
||||
}) async {
|
||||
var raw = _raw;
|
||||
if (raw == null || _needsReload) {
|
||||
LoadingHelper.showLoading();
|
||||
try {
|
||||
raw = await MineService.getVipProduct();
|
||||
} finally {
|
||||
LoadingHelper.dismissLoading(); //无论成功/异常都关 loading,避免请求抛异常时卡死
|
||||
}
|
||||
// 缓存供后续复用,但不触碰 isLoading/vipCards/notify,避免打扰已挂载的会员卡列表 UI
|
||||
if (raw != null && (raw.list?.isNotEmpty ?? false)) {
|
||||
_raw = raw;
|
||||
_needsReload = false;
|
||||
}
|
||||
}
|
||||
// 用当前预售状态现算列表(不依赖共享 vipCards,也不受其重拉影响)
|
||||
final list = _assemble(raw);
|
||||
if (list.isEmpty) {
|
||||
showToast("未获取到会员卡~");
|
||||
return;
|
||||
}
|
||||
final card = _pick(list, cardId) ??
|
||||
_pick(list, MineMsgProvider().payTier?.config?.vipCard) ??
|
||||
_pick(list, _abCardId) ??
|
||||
(globalStore.meInfo?.isUpgrade == true
|
||||
? _cheapestUpgrade(list)
|
||||
: null) ??
|
||||
list.first;
|
||||
if (card.rchgTypeUI.isEmpty) {
|
||||
showToast("未配置支付方式,请联系客服");
|
||||
return;
|
||||
}
|
||||
// 直拉支付弹窗没有 MineChargeVipLogic:需要卡皮埋点时自建 session,关弹窗报 CLOSE
|
||||
final analytics = reportAnalytics ? VipCardAnalyticsSession() : null;
|
||||
final base = orderTrack ?? const PayOrderTrackInfo();
|
||||
final track = PayOrderTrackInfo(
|
||||
sourcePage: base.sourcePage ?? PaySourcePage.unknown,
|
||||
sourceRef: base.sourceRef,
|
||||
videoId: base.videoId,
|
||||
activityId: base.activityId,
|
||||
sessionId: base.sessionId ?? analytics?.sessionId,
|
||||
//短剧付费墙的归因字段,重建时漏抄就丢了(同 OnlinePayPage._payByLink)
|
||||
mediaId: base.mediaId,
|
||||
contentId: base.contentId,
|
||||
checkoutContextId: base.checkoutContextId,
|
||||
// DISABLED:正常下单但不传实验字段
|
||||
experimentId: isExperimentActive
|
||||
? (base.experimentId ?? card.experimentId ?? experimentId)
|
||||
: null,
|
||||
experimentVariant: isExperimentActive
|
||||
? (base.experimentVariant ?? card.variant ?? variant)
|
||||
: null,
|
||||
);
|
||||
analytics?.reportPageViewAfterPaint();
|
||||
analytics?.reportProductImpression(card, afterPaint: true);
|
||||
try {
|
||||
await Get.bottomSheet(
|
||||
OnlinePayPage(vipProductModel: card, orderTrack: track),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} finally {
|
||||
analytics?.reportCloseWithoutPurchaseIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/// 多张可升级卡挑一张:待付价低优先,同价预售卡优先;无可升级卡返回 null
|
||||
VipProductModel? _cheapestUpgrade(List<VipProductModel> list) {
|
||||
final upgrades = list.where((e) => e.isUpgrade == true).toList();
|
||||
if (upgrades.isEmpty) return null;
|
||||
upgrades.sort((a, b) {
|
||||
final cmp = _upgradePrice(a).compareTo(_upgradePrice(b));
|
||||
if (cmp != 0) return cmp;
|
||||
return a.isPreSale ? -1 : 1; //同价预售卡优先
|
||||
});
|
||||
return upgrades.first;
|
||||
}
|
||||
|
||||
/// 升级比价用价:预售卡用实际待付(已付首款=尾款 balanceAmount / 未付=定金 advanceAmount),普通卡用 discountedPrice(对齐 mrhs)
|
||||
int _upgradePrice(VipProductModel card) {
|
||||
if (card.isPreSale) {
|
||||
return PreSaleProvider().isPayFirst
|
||||
? (card.balanceAmount ?? 0)
|
||||
: (card.advanceAmount ?? 0);
|
||||
}
|
||||
return card.discountedPrice ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 全站唯一实例(对齐 presaleProvider / globalStore 的顶层单例引用)
|
||||
final vipProductManager = VipProductManager();
|
||||
@@ -0,0 +1,778 @@
|
||||
//会员支持model
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
|
||||
import '../../../hj_model/mine/exchange/dc_model.dart';
|
||||
|
||||
/// /vip/product 实验状态:ACTIVE 生效,DISABLED 不可用(回原套餐,无实验字段)
|
||||
abstract class VipExperimentStatus {
|
||||
static const active = 'ACTIVE';
|
||||
static const disabled = 'DISABLED';
|
||||
}
|
||||
|
||||
/// uiConfig.badgeStyles 单项:按 badgeType 匹配角标底色/字色
|
||||
class VipBadgeStyle {
|
||||
String? badgeType;
|
||||
String? backgroundColor;
|
||||
String? textColor;
|
||||
|
||||
VipBadgeStyle({this.badgeType, this.backgroundColor, this.textColor});
|
||||
|
||||
VipBadgeStyle.fromJson(Map<String, dynamic>? json) {
|
||||
badgeType = json?['badgeType']?.toString();
|
||||
backgroundColor = json?['backgroundColor']?.toString();
|
||||
textColor = json?['textColor']?.toString();
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'badgeType': badgeType,
|
||||
'backgroundColor': backgroundColor,
|
||||
'textColor': textColor,
|
||||
};
|
||||
}
|
||||
|
||||
/// /vip/product.data.uiConfig:皮肤背景 + 角标样式表
|
||||
class VipUiConfig {
|
||||
String? backgroundImage;
|
||||
List<VipBadgeStyle>? badgeStyles;
|
||||
|
||||
VipUiConfig({this.backgroundImage, this.badgeStyles});
|
||||
|
||||
VipUiConfig.fromJson(Map<String, dynamic>? json) {
|
||||
backgroundImage = json?['backgroundImage']?.toString();
|
||||
final raw = json?['badgeStyles'];
|
||||
if (raw is List) {
|
||||
badgeStyles = raw
|
||||
.whereType<Map>()
|
||||
.map((e) => VipBadgeStyle.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'backgroundImage': backgroundImage,
|
||||
'badgeStyles': badgeStyles?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
/// 按 badgeType 查找样式(大小写不敏感)
|
||||
VipBadgeStyle? styleFor(String? badgeType) {
|
||||
final key = (badgeType ?? '').trim().toUpperCase();
|
||||
if (key.isEmpty) return null;
|
||||
for (final s in badgeStyles ?? const <VipBadgeStyle>[]) {
|
||||
if ((s.badgeType ?? '').trim().toUpperCase() == key) return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class VipSupportModel {
|
||||
DCModel? daichong;
|
||||
List<IntegralList>? integralList; //积分兑换列表
|
||||
bool? isNewUser;
|
||||
List<VipProductListModel>? list;
|
||||
List<VipProductModel>? upgradeableVipCardList;
|
||||
|
||||
/// A/B 实验状态:ACTIVE / DISABLED
|
||||
String? experimentStatus;
|
||||
|
||||
/// A/B 实验:实验 ID(下单时原样回传;DISABLED 时后端不返回)
|
||||
String? experimentId;
|
||||
|
||||
/// A/B 实验:分组 A / B(下单时原样回传)
|
||||
String? variant;
|
||||
|
||||
/// A/B 实验:默认选中卡 ID
|
||||
String? defaultProductId;
|
||||
|
||||
/// 皮肤 key,如 vip-card-skin-b(视觉皮肤标识,UI 分支仍以 variant 为准)
|
||||
String? skinKey;
|
||||
|
||||
/// 皮肤 UI 配置(背景图 / 角标样式表)
|
||||
VipUiConfig? uiConfig;
|
||||
|
||||
/// 实验是否生效(仅 ACTIVE 才挂实验字段 / 上报埋点 / 下单回传实验信息)
|
||||
bool get isExperimentActive => experimentStatus == VipExperimentStatus.active;
|
||||
|
||||
VipSupportModel({
|
||||
this.daichong,
|
||||
this.integralList,
|
||||
this.isNewUser,
|
||||
this.list,
|
||||
this.upgradeableVipCardList,
|
||||
this.experimentStatus,
|
||||
this.experimentId,
|
||||
this.variant,
|
||||
this.defaultProductId,
|
||||
this.skinKey,
|
||||
this.uiConfig,
|
||||
});
|
||||
|
||||
VipSupportModel.fromJson(Map<String, dynamic>? json) {
|
||||
daichong =
|
||||
json?['daichong'] != null ? DCModel.fromJson(json?['daichong']) : null;
|
||||
if (json?['integralList'] != null) {
|
||||
integralList = <IntegralList>[];
|
||||
json?['integralList'].forEach((v) {
|
||||
integralList!.add(IntegralList.fromJson(v));
|
||||
});
|
||||
}
|
||||
isNewUser = json?['isNewUser'];
|
||||
experimentStatus = json?['experimentStatus']?.toString();
|
||||
experimentId = json?['experimentId']?.toString();
|
||||
variant = json?['variant']?.toString();
|
||||
defaultProductId = json?['defaultProductId']?.toString();
|
||||
skinKey = json?['skinKey']?.toString();
|
||||
uiConfig = json?['uiConfig'] != null
|
||||
? VipUiConfig.fromJson(json?['uiConfig'])
|
||||
: null;
|
||||
|
||||
/// 升级卡
|
||||
upgradeableVipCardList = (json?['upgradeableVipCardList'] as List?)
|
||||
?.map((e) => VipProductModel.fromJson(e['product'] ?? {}))
|
||||
.toList();
|
||||
if (json?['list'] != null) {
|
||||
list = <VipProductListModel>[];
|
||||
json?['list'].forEach((v) {
|
||||
VipProductListModel vipModel =
|
||||
VipProductListModel.fromJson(v, daichong);
|
||||
list!.add(vipModel);
|
||||
});
|
||||
}
|
||||
// 仅 ACTIVE 时把实验上下文挂到卡上;DISABLED 时后端不带实验字段,也不 stamp
|
||||
if (isExperimentActive) {
|
||||
_stampAbExperiment();
|
||||
}
|
||||
}
|
||||
|
||||
void _stampAbExperiment() {
|
||||
void stamp(VipProductModel p) {
|
||||
p.experimentId = experimentId;
|
||||
p.variant = variant;
|
||||
}
|
||||
|
||||
for (final group in list ?? <VipProductListModel>[]) {
|
||||
for (final p in group.list ?? <VipProductModel>[]) {
|
||||
stamp(p);
|
||||
}
|
||||
}
|
||||
for (final p in upgradeableVipCardList ?? <VipProductModel>[]) {
|
||||
stamp(p);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (daichong != null) {
|
||||
data['daichong'] = daichong!.toJson();
|
||||
}
|
||||
if (integralList != null) {
|
||||
data['integralList'] = integralList!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['isNewUser'] = isNewUser;
|
||||
data['experimentStatus'] = experimentStatus;
|
||||
data['experimentId'] = experimentId;
|
||||
data['variant'] = variant;
|
||||
data['defaultProductId'] = defaultProductId;
|
||||
data['skinKey'] = skinKey;
|
||||
if (uiConfig != null) data['uiConfig'] = uiConfig!.toJson();
|
||||
if (list != null) {
|
||||
data['list'] = list!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class IntegralList {
|
||||
String? desc;
|
||||
int? duration;
|
||||
String? id;
|
||||
String? img;
|
||||
String? name;
|
||||
int? price;
|
||||
int? type;
|
||||
String? bgImg;
|
||||
|
||||
IntegralList(
|
||||
{this.desc,
|
||||
this.duration,
|
||||
this.id,
|
||||
this.img,
|
||||
this.name,
|
||||
this.price,
|
||||
this.type});
|
||||
|
||||
IntegralList.fromJson(Map<String, dynamic> json) {
|
||||
desc = json['desc'];
|
||||
duration = json['duration'];
|
||||
id = json['id'];
|
||||
img = json['img'];
|
||||
name = json['name'];
|
||||
price = json['price'];
|
||||
bgImg = json['bgImg'];
|
||||
type = json['type'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['desc'] = desc;
|
||||
data['duration'] = duration;
|
||||
data['id'] = id;
|
||||
data['img'] = img;
|
||||
data['name'] = name;
|
||||
data['price'] = price;
|
||||
data['type'] = type;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class VipProductListModel {
|
||||
List<VipProductModel>? list;
|
||||
String? position;
|
||||
String? positionID;
|
||||
int? showType;
|
||||
|
||||
VipProductListModel(
|
||||
{this.list, this.position, this.positionID, this.showType});
|
||||
|
||||
VipProductListModel.fromJson(Map<String, dynamic> json, DCModel? daichong) {
|
||||
if (json['list'] != null) {
|
||||
list = <VipProductModel>[];
|
||||
json['list'].forEach((v) {
|
||||
VipProductModel pModel = VipProductModel.fromJson(v);
|
||||
pModel.daichong = daichong;
|
||||
list!.add(pModel);
|
||||
});
|
||||
}
|
||||
position = json['position'];
|
||||
positionID = json['positionID'];
|
||||
showType = json['showType'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (list != null) {
|
||||
data['list'] = list!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['position'] = position;
|
||||
data['positionID'] = positionID;
|
||||
data['showType'] = showType;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class VipProductModel {
|
||||
String? actionDesc;
|
||||
|
||||
/// 角标类型,如 MOST_POPULAR
|
||||
String? badgeType;
|
||||
|
||||
/// 角标文案,如「最受欢迎」
|
||||
String? badgeText;
|
||||
int? aiUndressCount;
|
||||
String? alias;
|
||||
String? bgImg;
|
||||
int? chanSplitMod;
|
||||
int? chatPrice;
|
||||
String? createdAt;
|
||||
String? desc;
|
||||
int? discountedPrice;
|
||||
int? discountedPriceAnd;
|
||||
int? discountedPriceIos;
|
||||
int? downloadCount;
|
||||
int? duration;
|
||||
int? everyDayGiveCoin;
|
||||
String? exclusiveOffer;
|
||||
int? giveCoin;
|
||||
int? giveFruitCoin;
|
||||
int? goldVideoCouponCount;
|
||||
int? goldVideoCouponNum;
|
||||
int? goldVideoFreeDay;
|
||||
bool? isAmountPay; // true 支持金币支付
|
||||
String? newBgImg;
|
||||
String? newName;
|
||||
List<NewPrivilege>? newPrivilege;
|
||||
int? originalPrice;
|
||||
int? payVidDiscount;
|
||||
String? position;
|
||||
List<int>? privilege;
|
||||
String? privilegeDesc;
|
||||
String? productID;
|
||||
String? productName;
|
||||
int? productType; //21- 预售卡
|
||||
List<RchgType>? rchgType;
|
||||
int? serviceTime;
|
||||
int? showCountdownTime;
|
||||
int? signDays;
|
||||
int? sort;
|
||||
bool? status;
|
||||
int? timesAWeek;
|
||||
int? type; //会员卡类型 1、会员卡 2、礼包卡
|
||||
bool? unitPriceDisplay;
|
||||
String? updatedAt;
|
||||
int? videoDiscount;
|
||||
String? vipCardDesc;
|
||||
int? vipLevel;
|
||||
String? tag;
|
||||
DCModel? daichong; // 从上级数据结构手动赋值过来
|
||||
/// VIP 卡皮 A/B:由 /vip/product 响应挂到卡上,下单原样回传
|
||||
String? experimentId;
|
||||
String? variant;
|
||||
|
||||
//是否VIP升级
|
||||
bool? isUpgrade;
|
||||
//当前VIP卡名称
|
||||
String? currentVipName;
|
||||
//当前VIP卡价格
|
||||
String? currentVipPrice;
|
||||
// 原价购买价格
|
||||
int? purchasePrice;
|
||||
// 预付升级价格
|
||||
int? advanceAmount;
|
||||
// 预售尾款
|
||||
int? balanceAmount;
|
||||
|
||||
int get discountedPriceUI {
|
||||
return (discountedPrice ?? 0) ~/ 10;
|
||||
} // 现价 单位角(金币)
|
||||
|
||||
int? prepaidPrice;
|
||||
|
||||
int get prepaidPriceUI {
|
||||
return (prepaidPrice ?? 0) ~/ 10;
|
||||
}
|
||||
|
||||
int get originalPriceUI {
|
||||
return (originalPrice ?? 0) ~/ 10;
|
||||
} // 现价 单位角(金币)
|
||||
|
||||
bool isPreSale = false;
|
||||
|
||||
// 预售新增字段
|
||||
String? endBgImg;
|
||||
String? endBgSelectImg;
|
||||
|
||||
List<RchgType> get rchgTypeUI {
|
||||
List<RchgType> payList = [];
|
||||
List<String> payNameArr = [
|
||||
"支付宝(人工充值)",
|
||||
"微信(人工充值)",
|
||||
"银联(人工充值)",
|
||||
"信用卡(人工充值)",
|
||||
"花呗(人工充值)",
|
||||
"云闪付(人工充值)",
|
||||
"QQ錢包(人工充值)",
|
||||
"京东支付(人工充值)"
|
||||
];
|
||||
for (RchgType rechargeTypeBean in (rchgType ?? [])) {
|
||||
if (rechargeTypeBean.type == 'daichong') {
|
||||
if (daichong?.traders?.isNotEmpty == true) {
|
||||
PayForModel dcPayModel = daichong!.traders![0];
|
||||
if (dcPayModel.payInfos?.isNotEmpty == true) {
|
||||
for (PayInfoModel payInfoModel in dcPayModel.payInfos!) {
|
||||
var payType = RchgType();
|
||||
payType.isOfficial = true;
|
||||
payType.channel = rechargeTypeBean.channel;
|
||||
payType.incrAmount = rechargeTypeBean.incrAmount;
|
||||
payType.incTax = rechargeTypeBean.incTax;
|
||||
payType.payMethod = payInfoModel.payMethod;
|
||||
if (payInfoModel.payMethod == 101) {
|
||||
payType.type = 'alipy';
|
||||
payType.typeName = payNameArr[0];
|
||||
} else if (payInfoModel.payMethod == 102) {
|
||||
payType.type = 'wechat';
|
||||
payType.typeName = payNameArr[1];
|
||||
} else if (payInfoModel.payMethod == 103) {
|
||||
payType.type = 'union';
|
||||
payType.typeName = payNameArr[2];
|
||||
} else if (payInfoModel.payMethod == 104) {
|
||||
payType.type = 'credit';
|
||||
payType.typeName = payNameArr[3];
|
||||
} else if (payInfoModel.payMethod == 105) {
|
||||
payType.type = 'huabei';
|
||||
payType.typeName = payNameArr[4];
|
||||
} else if (payInfoModel.payMethod == 106) {
|
||||
payType.type = 'yunSanPay';
|
||||
payType.typeName = payNameArr[5];
|
||||
} else if (payInfoModel.payMethod == 107) {
|
||||
payType.type = 'qqWallet';
|
||||
payType.typeName = payNameArr[6];
|
||||
} else if (payInfoModel.payMethod == 108) {
|
||||
payType.type = 'jindongPay';
|
||||
payType.typeName = payNameArr[7];
|
||||
}
|
||||
payList.add(payType);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payList.add(rechargeTypeBean);
|
||||
}
|
||||
}
|
||||
if (isAmountPay == true) {
|
||||
RchgType coinType = RchgType();
|
||||
coinType.type = "coin";
|
||||
coinType.typeName = "金币";
|
||||
payList.add(coinType);
|
||||
}
|
||||
|
||||
return payList;
|
||||
}
|
||||
|
||||
VipProductModel({
|
||||
this.actionDesc,
|
||||
this.badgeType,
|
||||
this.badgeText,
|
||||
this.aiUndressCount,
|
||||
this.alias,
|
||||
this.bgImg,
|
||||
this.chanSplitMod,
|
||||
this.chatPrice,
|
||||
this.createdAt,
|
||||
this.desc,
|
||||
this.discountedPrice,
|
||||
this.discountedPriceAnd,
|
||||
this.discountedPriceIos,
|
||||
this.downloadCount,
|
||||
this.duration,
|
||||
this.everyDayGiveCoin,
|
||||
this.exclusiveOffer,
|
||||
this.giveCoin,
|
||||
this.giveFruitCoin,
|
||||
this.goldVideoCouponCount,
|
||||
this.goldVideoCouponNum,
|
||||
this.goldVideoFreeDay,
|
||||
this.isAmountPay,
|
||||
this.newBgImg,
|
||||
this.newName,
|
||||
this.newPrivilege,
|
||||
this.originalPrice,
|
||||
this.payVidDiscount,
|
||||
this.position,
|
||||
this.privilege,
|
||||
this.privilegeDesc,
|
||||
this.productID,
|
||||
this.productName,
|
||||
this.productType,
|
||||
this.rchgType,
|
||||
this.serviceTime,
|
||||
this.showCountdownTime,
|
||||
this.signDays,
|
||||
this.sort,
|
||||
this.status,
|
||||
this.timesAWeek,
|
||||
this.type,
|
||||
this.unitPriceDisplay,
|
||||
this.updatedAt,
|
||||
this.videoDiscount,
|
||||
this.vipCardDesc,
|
||||
this.vipLevel,
|
||||
this.isPreSale = false,
|
||||
this.tag,
|
||||
this.isUpgrade,
|
||||
this.currentVipName,
|
||||
this.currentVipPrice,
|
||||
this.purchasePrice,
|
||||
this.advanceAmount,
|
||||
});
|
||||
|
||||
VipProductModel.fromJson(Map<String, dynamic> json) {
|
||||
prepaidPrice = json['prepaidPrice'];
|
||||
endBgImg = json['endBgImg'];
|
||||
endBgSelectImg = json['endBgSelectImg'];
|
||||
actionDesc = json['actionDesc'];
|
||||
badgeType = json['badgeType'];
|
||||
badgeText = json['badgeText'];
|
||||
aiUndressCount = json['aiUndressCount'];
|
||||
alias = json['alias'];
|
||||
bgImg = json['bgImg'];
|
||||
chanSplitMod = json['chanSplitMod'];
|
||||
chatPrice = json['chatPrice'];
|
||||
createdAt = json['createdAt'];
|
||||
desc = json['desc'];
|
||||
discountedPrice = json['discountedPrice'];
|
||||
discountedPriceAnd = json['discountedPriceAnd'];
|
||||
discountedPriceIos = json['discountedPriceIos'];
|
||||
downloadCount = json['downloadCount'];
|
||||
duration = json['duration'];
|
||||
everyDayGiveCoin = json['everyDayGiveCoin'];
|
||||
exclusiveOffer = json['exclusiveOffer'];
|
||||
giveCoin = json['giveCoin'];
|
||||
giveFruitCoin = json['giveFruitCoin'];
|
||||
goldVideoCouponCount = json['goldVideoCouponCount'];
|
||||
goldVideoCouponNum = json['goldVideoCouponNum'];
|
||||
goldVideoFreeDay = json['goldVideoFreeDay'];
|
||||
isAmountPay = json['isAmountPay'];
|
||||
newBgImg = json['newBgImg'];
|
||||
newName = json['newName'];
|
||||
tag = json['tag'];
|
||||
if (json['newPrivilege'] != null) {
|
||||
newPrivilege = <NewPrivilege>[];
|
||||
json['newPrivilege'].forEach((v) {
|
||||
newPrivilege!.add(NewPrivilege.fromJson(v));
|
||||
});
|
||||
}
|
||||
originalPrice = json['originalPrice'];
|
||||
payVidDiscount = json['payVidDiscount'];
|
||||
position = json['position'];
|
||||
if (json['privilege'] != null) {
|
||||
privilege = (json['privilege'] as List?)?.map((v) {
|
||||
return int.tryParse(v.toString()) ?? 0;
|
||||
}).toList();
|
||||
}
|
||||
privilegeDesc = json['privilegeDesc'];
|
||||
productID = json['productID'];
|
||||
productName = json['productName'];
|
||||
productType = json['productType'];
|
||||
if (json['rchgType'] != null) {
|
||||
rchgType = <RchgType>[];
|
||||
json['rchgType'].forEach((v) {
|
||||
rchgType!.add(RchgType.fromJson(v));
|
||||
});
|
||||
}
|
||||
serviceTime = json['serviceTime'];
|
||||
showCountdownTime = json['showCountdownTime'];
|
||||
signDays = json['signDays'];
|
||||
sort = json['sort'];
|
||||
status = json['status'];
|
||||
timesAWeek = json['timesAWeek'];
|
||||
type = json['type'];
|
||||
unitPriceDisplay = json['unitPriceDisplay'];
|
||||
updatedAt = json['updatedAt'];
|
||||
videoDiscount = json['videoDiscount'];
|
||||
vipCardDesc = json['vipCardDesc'];
|
||||
vipLevel = json['vipLevel'];
|
||||
isPreSale = false;
|
||||
isUpgrade = json['isUpgrade'];
|
||||
currentVipName = json['currentVipName'];
|
||||
currentVipPrice = "${json['currentVipPrice']}";
|
||||
purchasePrice = json['purchasePrice'];
|
||||
advanceAmount = json['advanceAmount'];
|
||||
balanceAmount = json['balanceAmount'];
|
||||
}
|
||||
|
||||
//是否有折扣
|
||||
bool hasDiscout() {
|
||||
return discountedPrice != originalPrice;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['actionDesc'] = actionDesc;
|
||||
data['badgeType'] = badgeType;
|
||||
data['badgeText'] = badgeText;
|
||||
data['aiUndressCount'] = aiUndressCount;
|
||||
data['alias'] = alias;
|
||||
data['bgImg'] = bgImg;
|
||||
data['chanSplitMod'] = chanSplitMod;
|
||||
data['chatPrice'] = chatPrice;
|
||||
data['createdAt'] = createdAt;
|
||||
data['desc'] = desc;
|
||||
data['discountedPrice'] = discountedPrice;
|
||||
data['discountedPriceAnd'] = discountedPriceAnd;
|
||||
data['discountedPriceIos'] = discountedPriceIos;
|
||||
data['downloadCount'] = downloadCount;
|
||||
data['duration'] = duration;
|
||||
data['everyDayGiveCoin'] = everyDayGiveCoin;
|
||||
data['exclusiveOffer'] = exclusiveOffer;
|
||||
data['giveCoin'] = giveCoin;
|
||||
data['giveFruitCoin'] = giveFruitCoin;
|
||||
data['goldVideoCouponCount'] = goldVideoCouponCount;
|
||||
data['goldVideoCouponNum'] = goldVideoCouponNum;
|
||||
data['goldVideoFreeDay'] = goldVideoFreeDay;
|
||||
data['isAmountPay'] = isAmountPay;
|
||||
data['newBgImg'] = newBgImg;
|
||||
data['newName'] = newName;
|
||||
if (newPrivilege != null) {
|
||||
data['newPrivilege'] = newPrivilege!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['originalPrice'] = originalPrice;
|
||||
data['payVidDiscount'] = payVidDiscount;
|
||||
data['position'] = position;
|
||||
if (privilege != null) {
|
||||
data['privilege'] = privilege!.map((v) => v).toList();
|
||||
}
|
||||
data['privilegeDesc'] = privilegeDesc;
|
||||
data['productID'] = productID;
|
||||
data['productName'] = productName;
|
||||
data['productType'] = productType;
|
||||
if (rchgType != null) {
|
||||
data['rchgType'] = rchgType!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['serviceTime'] = serviceTime;
|
||||
data['showCountdownTime'] = showCountdownTime;
|
||||
data['signDays'] = signDays;
|
||||
data['sort'] = sort;
|
||||
data['status'] = status;
|
||||
data['timesAWeek'] = timesAWeek;
|
||||
data['type'] = type;
|
||||
data['unitPriceDisplay'] = unitPriceDisplay;
|
||||
data['updatedAt'] = updatedAt;
|
||||
data['videoDiscount'] = videoDiscount;
|
||||
data['vipCardDesc'] = vipCardDesc;
|
||||
data['vipLevel'] = vipLevel;
|
||||
data['isUpgrade'] = isUpgrade;
|
||||
data['currentVipName'] = currentVipName;
|
||||
data['currentVipPrice'] = currentVipPrice;
|
||||
data['purchasePrice'] = purchasePrice;
|
||||
data['advanceAmount'] = advanceAmount;
|
||||
data['balanceAmount'] = balanceAmount;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
extension PreSale on VipProductModel {
|
||||
String realNormalVipImage() {
|
||||
if (!isPreSale) return bgImg ?? '';
|
||||
if (PreSaleProvider().canPayBalance) return endBgImg ?? '';
|
||||
return bgImg ?? '';
|
||||
}
|
||||
|
||||
String realSelectVipImage() {
|
||||
if (!isPreSale) return newBgImg ?? '';
|
||||
if (PreSaleProvider().canPayBalance) return endBgSelectImg ?? '';
|
||||
return newBgImg ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
class NewPrivilege {
|
||||
String? id;
|
||||
String? img;
|
||||
String? privilegeDesc;
|
||||
String? privilegeName;
|
||||
int? privilege;
|
||||
String? uncheckedImg;
|
||||
bool? isCore; //是否核心权益(true 进「我的核心权益」横向卡片,false 进「更多权益」网格)
|
||||
|
||||
NewPrivilege({this.img, this.privilegeDesc, this.privilegeName});
|
||||
|
||||
NewPrivilege.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
img = json['img'];
|
||||
privilegeDesc = json['privilegeDesc'];
|
||||
privilegeName = json['privilegeName'];
|
||||
privilege = json['privilege'];
|
||||
uncheckedImg = json['uncheckedImg'];
|
||||
isCore = json['isCore'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['img'] = img;
|
||||
data['privilegeDesc'] = privilegeDesc;
|
||||
data['privilegeName'] = privilegeName;
|
||||
data['uncheckedImg'] = uncheckedImg;
|
||||
data['privilege'] = privilege;
|
||||
data['isCore'] = isCore;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class RchgType {
|
||||
String? channel; //渠道类型 鲨鱼 金鱼
|
||||
int? incTax; //按比率增加额外优惠额 0-1之间 ,如果 incrAmount 与 incrTax 同时存在 以 incrAmount 为准
|
||||
int? incrAmount; //增加的优惠额度
|
||||
String? type; //充值方式 //coin 金币方式
|
||||
String? typeName; //支付宝,微信,银联
|
||||
|
||||
//daichong 业务字段
|
||||
bool? isOfficial = false; // 官方推荐
|
||||
int? payMethod;
|
||||
|
||||
RchgType(
|
||||
{this.channel, this.incTax, this.incrAmount, this.type, this.typeName});
|
||||
|
||||
RchgType.fromJson(Map<String, dynamic> json) {
|
||||
channel = json['channel'];
|
||||
incTax = json['incTax'];
|
||||
incrAmount = json['incrAmount'];
|
||||
type = json['type'];
|
||||
typeName = json['typeName'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['channel'] = channel;
|
||||
data['incTax'] = incTax;
|
||||
data['incrAmount'] = incrAmount;
|
||||
data['type'] = type;
|
||||
data['typeName'] = typeName;
|
||||
return data;
|
||||
}
|
||||
|
||||
String getPayIcon() {
|
||||
if (isOfficial == true) {
|
||||
// 代充
|
||||
return "pay_icon103.png".mineImgPath;
|
||||
} else if (type == 'alipay') {
|
||||
return "ic_alipay.png".mineImgPath;
|
||||
} else if (type == 'union') {
|
||||
return "pay_icon103.png".mineImgPath;
|
||||
} else if (type == 'wechat') {
|
||||
return "ic_wechat.png".mineImgPath; //AssetsSvg.SVG_PAY_ICON102;
|
||||
} else if (type == 'coin') {
|
||||
//金币
|
||||
return "ic_coin.webp".mineImgPath; //AssetsSvg.SVG_PAY_ICON102;
|
||||
} else if (type == 'usdt') {
|
||||
return "ic_usdt.png".mineImgPath;
|
||||
} else {
|
||||
return "";
|
||||
// return "ic_coupon.png".mineImgPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 加赠券
|
||||
|
||||
class CouponModel {
|
||||
String? cId;
|
||||
int? count;
|
||||
String? createTime;
|
||||
String? expireTime;
|
||||
String? id;
|
||||
String? name;
|
||||
int? price;
|
||||
int? type;
|
||||
bool? used;
|
||||
int? value;
|
||||
|
||||
CouponModel(
|
||||
{this.cId,
|
||||
this.count,
|
||||
this.createTime,
|
||||
this.expireTime,
|
||||
this.id,
|
||||
this.name,
|
||||
this.price,
|
||||
this.type,
|
||||
this.used,
|
||||
this.value});
|
||||
|
||||
CouponModel.fromJson(Map<String, dynamic> json) {
|
||||
cId = json['cId'];
|
||||
count = json['count'];
|
||||
createTime = json['createTime'];
|
||||
expireTime = json['expireTime'];
|
||||
id = json['id'];
|
||||
name = json['name'];
|
||||
price = json['price'];
|
||||
type = json['type'];
|
||||
used = json['used'];
|
||||
value = json['value'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['cId'] = this.cId;
|
||||
data['count'] = this.count;
|
||||
data['createTime'] = this.createTime;
|
||||
data['expireTime'] = this.expireTime;
|
||||
data['id'] = this.id;
|
||||
data['name'] = this.name;
|
||||
data['price'] = this.price;
|
||||
data['type'] = this.type;
|
||||
data['used'] = this.used;
|
||||
data['value'] = this.value;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
/// VIP 金色标题渐变 #F5E7BC → #DCAD55
|
||||
/// 会员中心 / 购买弹窗 / 权益标题等金色文案共用,调色只改此处
|
||||
const LinearGradient kVipTitleGradient = LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xffF5E7BC), Color(0xffDCAD55)],
|
||||
);
|
||||
|
||||
/// 金渐变着色包装:给文字 / 线条 / 菱形套上 [kVipTitleGradient]
|
||||
class VipGradientMask extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const VipGradientMask({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) => kVipTitleGradient.createShader(bounds),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// B 组「核心权益」标题图(会员中心 / 购买弹窗共用)
|
||||
class VipCoreSectionTitleImage extends StatelessWidget {
|
||||
const VipCoreSectionTitleImage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
'vip_special_title.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// B 组「更多权益」标题图(会员中心 / 购买弹窗共用)
|
||||
class VipMoreSectionTitleImage extends StatelessWidget {
|
||||
const VipMoreSectionTitleImage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Image.asset(
|
||||
'vip_more_title.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fitWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 区块标题:两侧「金渐变横线 + 菱形」+ 居中金渐变文字(会员中心 B / 购买弹窗共用)
|
||||
class VipSectionTitle extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
/// true:旧版橙色标题(会员中心 A / DISABLED)
|
||||
final bool classic;
|
||||
|
||||
const VipSectionTitle(this.text, {super.key, this.classic = false});
|
||||
|
||||
static const _classicOrange = Color(0xffF68804);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (classic) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_classicDeco(lineFirst: true),
|
||||
8.sizeBoxW,
|
||||
Text(text,
|
||||
style: const TextStyle(
|
||||
color: _classicOrange,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600)),
|
||||
8.sizeBoxW,
|
||||
_classicDeco(lineFirst: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildDeco(lineFirst: true),
|
||||
8.sizeBoxW,
|
||||
VipGradientMask(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 20, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
_buildDeco(lineFirst: false),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _classicDeco({required bool lineFirst}) {
|
||||
final line = Container(width: 40, height: 0.5, color: _classicOrange);
|
||||
final diamond = Transform.rotate(
|
||||
angle: 0.7853981633974483,
|
||||
child: Container(width: 5, height: 5, color: _classicOrange),
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: lineFirst ? [line, diamond] : [diamond, line],
|
||||
);
|
||||
}
|
||||
|
||||
// 标题两侧装饰:金渐变细横线 + 菱形(lineFirst=true 横线在外侧,false 镜像)
|
||||
Widget _buildDeco({required bool lineFirst}) {
|
||||
const line = VipGradientMask(
|
||||
child: SizedBox(
|
||||
width: 40, height: 0.5, child: ColoredBox(color: Colors.white)),
|
||||
);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: lineFirst ? [line, _diamond] : [_diamond, line],
|
||||
);
|
||||
}
|
||||
|
||||
// 金渐变小菱形装饰(45° 旋转的小方块)
|
||||
Widget get _diamond => Transform.rotate(
|
||||
angle: 0.7853981633974483,
|
||||
child: const VipGradientMask(
|
||||
child: SizedBox(
|
||||
width: 5, height: 5, child: ColoredBox(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/mine/exchange/recharge_type_list_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
// 金币充值档位卡片
|
||||
class CoinItem extends StatelessWidget {
|
||||
final RechargeTypeModel model;
|
||||
final bool isSelected; // 是否选中
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const CoinItem(this.model, {super.key, this.onTap, this.isSelected = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final couponDesc = model.couponDesc;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: isSelected
|
||||
? Border.all(width: 1, color: const Color(0xffF9C142))
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
"${model.amount}金币",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
Text(
|
||||
'¥${model.moneyYuan}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF9C142), fontWeight: FontWeight.w500),
|
||||
),
|
||||
4.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
// 左上角优惠角标
|
||||
if (couponDesc?.isNotEmpty == true)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
height: 20,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12)),
|
||||
color: Color(0xffFFD460),
|
||||
),
|
||||
child: Text(
|
||||
couponDesc!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0xff292929),
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
fontSize: 10.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
|
||||
import '../../make_money/mine_withdrawal_record_page.dart';
|
||||
import '../mine_charge_coin_logic.dart';
|
||||
import '../pay_order_source.dart';
|
||||
import 'coin_item.dart';
|
||||
|
||||
/// 金币支付底部弹窗(按设计稿:余额卡片 + 档位网格 + 立即支付)
|
||||
class CoinPayBottomSheet extends StatelessWidget {
|
||||
/// 下单来源,默认播放页底部弹窗——本弹窗只在播放页(购买弹窗/解锁蒙层)用
|
||||
final PaySourcePage sourcePage;
|
||||
|
||||
/// 下单埋点上下文(短剧付费墙充金币要带剧/集/付费墙上下文)
|
||||
final PayOrderTrackInfo? orderTrack;
|
||||
|
||||
const CoinPayBottomSheet(
|
||||
{super.key,
|
||||
this.sourcePage = PaySourcePage.videoBottomSheet,
|
||||
this.orderTrack});
|
||||
|
||||
/// 弹窗展示中,防连点叠多个 bottomSheet
|
||||
static bool _isShowing = false;
|
||||
|
||||
static Future<T?> show<T>({
|
||||
PaySourcePage sourcePage = PaySourcePage.videoBottomSheet,
|
||||
PayOrderTrackInfo? orderTrack,
|
||||
}) async {
|
||||
if (_isShowing) return null;
|
||||
_isShowing = true;
|
||||
try {
|
||||
return await Get.bottomSheet<T>(
|
||||
CoinPayBottomSheet(sourcePage: sourcePage, orderTrack: orderTrack),
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
} finally {
|
||||
_isShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineChargeCoinLogic>(
|
||||
init: MineChargeCoinLogic(sourcePage: sourcePage, orderTrack: orderTrack),
|
||||
global: false,
|
||||
builder: (logic) {
|
||||
// Material 填深色底:圆角抗锯齿对着 #040018,避免透明底导致左右上角异色
|
||||
return Material(
|
||||
color: const Color(0xff040018),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxHeight: Get.height * 0.85),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xff040018), Color(0xff060606)],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 顶部径向黄光:#FACC15 16% → 0%
|
||||
const Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
center: Alignment.topCenter,
|
||||
radius: 1.2,
|
||||
colors: [
|
||||
Color(0x29FACC15), // #FACC15 @ 16%
|
||||
Color(0x00FACC15), // #FACC15 @ 0%
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
const SheetHandleBar(),
|
||||
16.sizeBoxH,
|
||||
const Text(
|
||||
'金币支付',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
16.sizeBoxH,
|
||||
Flexible(child: _buildBody(logic)),
|
||||
_buildBottomBar(logic),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(MineChargeCoinLogic logic) {
|
||||
if (logic.isInitLoading) {
|
||||
return const SizedBox(height: 220, child: LoadingCenterWidget());
|
||||
}
|
||||
if (logic.model?.list?.isEmpty != false) {
|
||||
return SizedBox(
|
||||
height: 220,
|
||||
child: CErrorWidget(retryOnTap: () => logic.loadData()),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildWallet(),
|
||||
16.sizeBoxH,
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: 111 / 138,
|
||||
),
|
||||
itemCount: logic.model!.list!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = logic.model!.list![index];
|
||||
final isSelected = logic.selectedCoin?.id == model.id;
|
||||
return CoinItem(
|
||||
model,
|
||||
isSelected: isSelected,
|
||||
onTap: () => logic.onSelectCoin(index),
|
||||
);
|
||||
},
|
||||
),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 我的金币余额卡片
|
||||
Widget _buildWallet() {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'我的金币余额',
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
8.sizeBoxH,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('coin_icon.webp'.mineImgPath, width: 36),
|
||||
4.sizeBoxW,
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, store, __) {
|
||||
final wallet = store.wallet;
|
||||
final total = (wallet?.amount ?? 0) + (wallet?.income ?? 0);
|
||||
return Text(
|
||||
'$total',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffFFD460),
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () =>
|
||||
Get.to(RecordsPage(RecordType.bill), opaque: false),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: 30,
|
||||
width: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffFFD460),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: const Text(
|
||||
'余额明细',
|
||||
style: TextStyle(color: Color(0xff3D3D3D), fontSize: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 立即支付 + 在线客服
|
||||
Widget _buildBottomBar(MineChargeCoinLogic logic) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => logic.onGotoPay(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.actionRed,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'¥${logic.selectedCoin?.moneyYuan ?? 0}/立即支付',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('支付问题反馈,点击联系 ',
|
||||
style: textStyle(12, const Color(0xffBFBFC1), FontWeight.w400)),
|
||||
GestureDetector(
|
||||
onTap: pushToCustomService,
|
||||
child: Text('在线客服',
|
||||
style:
|
||||
textStyle(12, const Color(0xffFFD460), FontWeight.w400)),
|
||||
),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
class FeedbackQuestionCategoryView extends StatefulWidget {
|
||||
final Function(String category)? choseCategory;
|
||||
const FeedbackQuestionCategoryView({super.key, this.choseCategory});
|
||||
|
||||
@override
|
||||
State<FeedbackQuestionCategoryView> createState() =>
|
||||
_FeedbackQuestionCategoryViewState();
|
||||
}
|
||||
|
||||
class _FeedbackQuestionCategoryViewState
|
||||
extends State<FeedbackQuestionCategoryView> {
|
||||
final dataSource = [
|
||||
'账号问题',
|
||||
'影视资源',
|
||||
'APP体验',
|
||||
'播放失败',
|
||||
'播放卡顿',
|
||||
'分类有误',
|
||||
'充值问题',
|
||||
'其他'
|
||||
];
|
||||
String? selectCategory;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 5,
|
||||
childAspectRatio: 84 / 34),
|
||||
itemCount: dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final category = dataSource[index];
|
||||
final select = category == selectCategory;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
selectCategory = category;
|
||||
setState(() {});
|
||||
widget.choseCategory?.call(category);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: select
|
||||
? Color(0xFFF68804)
|
||||
: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
category,
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InfomationInputView extends StatefulWidget {
|
||||
final String title;
|
||||
final String hint;
|
||||
final TextEditingController controller;
|
||||
const InfomationInputView(this.controller,
|
||||
{super.key, this.title = '', this.hint = ''});
|
||||
|
||||
@override
|
||||
State<InfomationInputView> createState() => _InfomationInputViewState();
|
||||
}
|
||||
|
||||
class _InfomationInputViewState extends State<InfomationInputView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 90,
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
maxLength: 20,
|
||||
controller: widget.controller,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: widget.hint,
|
||||
hintStyle:
|
||||
TextStyle(color: Color(0xff525252), fontSize: 12),
|
||||
counterText: '',
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Divider(
|
||||
height: 0.5,
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/tools_base/file_upload/file_upload_tool.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
|
||||
|
||||
class MineFeedbackLogic extends GetxController {
|
||||
/// 问题描述
|
||||
TextEditingController inputFeedback = TextEditingController();
|
||||
|
||||
/// 区域
|
||||
TextEditingController areaFeedback = TextEditingController();
|
||||
|
||||
/// 设备
|
||||
TextEditingController deviceFeedback = TextEditingController();
|
||||
|
||||
/// 网络运营商
|
||||
TextEditingController netFeedback = TextEditingController();
|
||||
|
||||
///联系方式
|
||||
TextEditingController contactFeedback = TextEditingController();
|
||||
|
||||
///封面本地地址
|
||||
List<String> _localPicList = [];
|
||||
|
||||
String _questionCategory = '';
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
inputFeedback.dispose();
|
||||
areaFeedback.dispose();
|
||||
deviceFeedback.dispose();
|
||||
netFeedback.dispose();
|
||||
contactFeedback.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 主要问题
|
||||
updateQuestionCategory(String category) => _questionCategory = category;
|
||||
|
||||
/// 资源图片变化
|
||||
updateQuestionImages(List<String> images) => _localPicList = images;
|
||||
|
||||
Future<void> onSubmit() async {
|
||||
if (_questionCategory.isEmpty) {
|
||||
showToast("请选择遇到的问题分类");
|
||||
return;
|
||||
}
|
||||
if (inputFeedback.text.isEmpty) {
|
||||
showToast("请填写问题描述");
|
||||
return;
|
||||
}
|
||||
|
||||
// 图片可选:无图直接提交;有图先上传再提交,上传失败仍照常提交(不阻断反馈)
|
||||
if (_localPicList.isEmpty) {
|
||||
_submitFeedback([]);
|
||||
return;
|
||||
}
|
||||
FileUploadTool().uploadImagesWithProgress(
|
||||
_localPicList,
|
||||
onSuccess: (urls) => _submitFeedback(urls),
|
||||
onFailure: () => _submitFeedback([]),
|
||||
);
|
||||
}
|
||||
|
||||
/// 提交反馈([images] 为已上传的图片 url,可为空)
|
||||
Future<void> _submitFeedback(List<String> images) async {
|
||||
LoadingAlertWidget.show(title: "正在提交...");
|
||||
try {
|
||||
FocusScope.of(Get.context!).unfocus();
|
||||
final success = await MineService.feedback(inputFeedback.text,
|
||||
location: areaFeedback.text,
|
||||
device: deviceFeedback.text,
|
||||
carrier: netFeedback.text,
|
||||
img: images,
|
||||
fType: _questionCategory,
|
||||
contact: contactFeedback.text);
|
||||
if (success) {
|
||||
Get.back();
|
||||
showToast('提交成功');
|
||||
} else {
|
||||
showToast('提交失败');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
showToast(e.message ?? '');
|
||||
} catch (e) {
|
||||
showToast(e.toString());
|
||||
} finally {
|
||||
LoadingAlertWidget.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import 'feedback_question_category_view.dart';
|
||||
import 'mine_feedback_logic.dart';
|
||||
import 'mine_qa_page.dart';
|
||||
import 'photo_manage_view.dart';
|
||||
|
||||
class MineFeedbackPage extends StatelessWidget {
|
||||
const MineFeedbackPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineFeedbackLogic>(
|
||||
init: MineFeedbackLogic(),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('意见反馈'),
|
||||
actions: [
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(() => MineQAPage()),
|
||||
child: Text(
|
||||
'Q&A',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.45),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 10, right: 10, top: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'遇到的问题',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
FeedbackQuestionCategoryView(
|
||||
choseCategory: controller.updateQuestionCategory,
|
||||
),
|
||||
24.sizeBoxH,
|
||||
Text(
|
||||
'问题描述(必填)',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
constraints: BoxConstraints(minHeight: 160),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 11),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TextField(
|
||||
maxLines: 8,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 12),
|
||||
maxLength: 300,
|
||||
controller: controller.inputFeedback,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText:
|
||||
'请详细描述您的问题,无效信息无法帮助技术人员排查问题。
有效信息:机型,系统,地区,问题症状...',
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xff525252), fontSize: 12),
|
||||
counterStyle: TextStyle(fontSize: 8),
|
||||
counter: SizedBox(),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
// 字数计数:只监听不接管所有权(释放归 MineFeedbackLogic.onClose)
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.inputFeedback,
|
||||
builder: (_, value, __) => Text(
|
||||
'${value.text.length}/300',
|
||||
style: TextStyle(
|
||||
color:
|
||||
Colors.black.withValues(alpha: 0.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
14.sizeBoxH,
|
||||
InfomationInputView(controller.areaFeedback,
|
||||
title: '所在地区', hint: '例:浙江杭州'),
|
||||
InfomationInputView(controller.deviceFeedback,
|
||||
title: '设备信息', hint: '例:苹果14'),
|
||||
InfomationInputView(controller.netFeedback,
|
||||
title: '网络运营商', hint: '电信/联通/移动'),
|
||||
InfomationInputView(controller.contactFeedback,
|
||||
title: '联系方式', hint: 'QQ/微信/邮箱等'),
|
||||
24.sizeBoxH,
|
||||
Text(
|
||||
'上传图片',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
PhotoManageView(
|
||||
resourceOnchanged: controller.updateQuestionImages,
|
||||
),
|
||||
24.sizeBoxH,
|
||||
Text(
|
||||
'以方便我们给您回复,有效的改进建议,有惊喜赠送哟!',
|
||||
style:
|
||||
TextStyle(color: Color(0xff525252), fontSize: 12),
|
||||
),
|
||||
60.sizeBoxH,
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: controller.onSubmit,
|
||||
child: Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 45),
|
||||
alignment: Alignment.center,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
'提交意见',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
20.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
|
||||
///帮助反馈
|
||||
class MineQAPage extends StatefulWidget {
|
||||
const MineQAPage({super.key});
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MineQAPageState();
|
||||
}
|
||||
|
||||
class _MineQAPageState extends State<MineQAPage> {
|
||||
final String tg = 'https://t.me/mu02guang';
|
||||
final String email = 'xzhan5555@gmail.com';
|
||||
final String url = 'XV9.FM';
|
||||
|
||||
late final questionList = [
|
||||
{
|
||||
"question": "描述文件安装失败?",
|
||||
"answer": "若提示【新的MDM有效负载与旧的有效负载不匹配】,请移除移动设备管理步骤:【设置-通用-设备管理-移动设备管理-移除管理】",
|
||||
'isOpen': true
|
||||
},
|
||||
{
|
||||
"question": "怎么找回账号?",
|
||||
"answer":
|
||||
"本平台登录会自动创建账号,需保存账号凭证或绑定手机号码,才能记录之前的账号信息。受行业限制,APP无法正常使用时需升级,未绑定手机号码会导致账号信息丢失。请及时绑定手机号码或保存账号凭证,以免VIP信息丢失,造成巨大财产损失!账号丢失的用户可在 『我的』页面-账号找回,原账号的VIP信息会转移至新账号上。『账号凭证』『手机绑定』都没有的情况下,如账号VIP信息丢失,可以提供VIP支付充值凭证截图联系在线客服为您查询核实恢复VIP。",
|
||||
'isOpen': false
|
||||
},
|
||||
{
|
||||
"question": "怎么支付不成功?",
|
||||
"answer":
|
||||
"1.因超时支付无法到账,请重新发起。\n2.每天发起支付不能超过5次,连续发起且未支付,账号可能被加入黑名单。\n3.支付通道在夜间比较忙碌,尝试多次发起,后台会为您自动切换不同支付通道。\n4.若充值成功,用户权益通常会在数分钟内到账,请刷新APP或重启。\n6.若支付成功并重启APP后依然没有到账,请联系在线客服并提供付款成功凭证截图。",
|
||||
'isOpen': false
|
||||
},
|
||||
{
|
||||
"question": "收到手机报毒提醒?",
|
||||
"answer":
|
||||
"本平台有主要收益为广告赞助,且保证APP安全无毒,因平台主要展示内容为色情属于特殊行业,某些杀毒软件会误报毒提醒,如遇此类提醒请忽略继续使用。",
|
||||
'isOpen': false
|
||||
},
|
||||
{
|
||||
"question": "联系方式?",
|
||||
"answer": "商务合作TG: $tg\n官方邮箱: $email\n永久下载地址: $url",
|
||||
'isOpen': false
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("常见问题"),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...List.generate(
|
||||
questionList.length,
|
||||
(index) => _getItem(index),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getItem(int index) {
|
||||
final item = questionList[index];
|
||||
final isOpen = item["isOpen"] as bool;
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
item["isOpen"] = !isOpen;
|
||||
setState(() {});
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
item["question"] as String,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Transform.rotate(
|
||||
angle: isOpen ? (pi * -0.5) : 0,
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios_rounded,
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isOpen) ...[
|
||||
10.sizeBoxH,
|
||||
EasyRichText(
|
||||
'${item["answer"] as String}',
|
||||
defaultStyle: TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: tg,
|
||||
style: TextStyle(color: AppColors.primaryHighColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlToWeb(tg);
|
||||
},
|
||||
),
|
||||
EasyRichTextPattern(
|
||||
targetString: url,
|
||||
style: TextStyle(color: AppColors.primaryHighColor),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrlToWeb('https://$url');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
12.sizeBoxH,
|
||||
Divider(
|
||||
height: .5,
|
||||
color: Colors.black.withValues(alpha: .04),
|
||||
),
|
||||
19.sizeBoxH,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_pickers/image_pickers.dart';
|
||||
import 'package:hgdj/tools_base/widget/add_media_source_button.dart';
|
||||
|
||||
class PhotoManageView extends StatefulWidget {
|
||||
final Function(List<String> resources)? resourceOnchanged;
|
||||
final int max;
|
||||
const PhotoManageView({super.key, this.resourceOnchanged, this.max = 9});
|
||||
|
||||
@override
|
||||
State<PhotoManageView> createState() => _PhotoManageViewState();
|
||||
}
|
||||
|
||||
class _PhotoManageViewState extends State<PhotoManageView> {
|
||||
final dataSource = <String>[];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 1),
|
||||
itemCount: min(dataSource.length + 1, 9),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == dataSource.length)
|
||||
return AddMediaSourceButton(
|
||||
isVideo: false,
|
||||
onTap: _addAlbumPhotos,
|
||||
backgroundColor: Colors.black.withValues(alpha: .04),
|
||||
);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.file(
|
||||
File(dataSource[index]),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
right: 6,
|
||||
top: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
dataSource.removeAt(index);
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .8),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 14,
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_addAlbumPhotos() async {
|
||||
// image_picker 走系统相册 intent,选图不需要存储权限,直接调起
|
||||
final listMedia = await ImagePickers.pickerPaths(
|
||||
uiConfig: UIConfig(uiThemeColor: Colors.white),
|
||||
galleryMode: GalleryMode.image,
|
||||
selectCount: 9 - dataSource.length,
|
||||
showCamera: false,
|
||||
);
|
||||
if (listMedia.isEmpty) return;
|
||||
final ret = listMedia.map((e) => e.path ?? '').toList();
|
||||
dataSource.addAll(ret);
|
||||
setState(() {});
|
||||
widget.resourceOnchanged?.call(dataSource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
|
||||
import '../../../hj_model/mine/happy/happy_model.dart';
|
||||
import '../../../hj_model/splash/ads_model.dart';
|
||||
|
||||
/// 应用推荐页:按分类拉广告位数据,每个分类一个 tab
|
||||
class LouFengAdLogic extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
bool isLoading = true;
|
||||
HappyModel? model; // null 表示加载失败,页面展示重试
|
||||
|
||||
final tabs = <String>[]; // 有内容的分类名
|
||||
final configs = <AdTabConfig>[]; // 与 tabs 一一对应的渲染配置
|
||||
TabController? tabCtr;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
tabCtr?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 拉数据并重建分类,失败时 model 为 null 由页面兜底
|
||||
Future<void> loadData() async {
|
||||
model = await CommonService.happyList();
|
||||
isLoading = false;
|
||||
tabs.clear();
|
||||
configs.clear();
|
||||
if (model != null) _buildTabs();
|
||||
update();
|
||||
}
|
||||
|
||||
/// 「应用」固定展示,其余分类有 banner 或有应用才建 tab
|
||||
void _buildTabs() {
|
||||
_add(
|
||||
'应用',
|
||||
AdTabConfig(
|
||||
bannerAds: _banners(1),
|
||||
hengAds: model?.hengApp != null
|
||||
? AdGroup(items: model?.hengApp, title: '官方推荐')
|
||||
: null,
|
||||
shuAds: model?.shuApp != null
|
||||
? AdGroup(items: model?.shuApp, title: '热门应用')
|
||||
: null,
|
||||
));
|
||||
|
||||
final others = [
|
||||
('炮台', 3, model?.ypApp),
|
||||
('棋牌', 4, model?.qpApp),
|
||||
('直播', 5, model?.zbApp),
|
||||
('游戏', 2, model?.gameApp),
|
||||
];
|
||||
for (final (name, moduleType, apps) in others) {
|
||||
final banners = _banners(moduleType);
|
||||
if (banners.isEmpty && (apps?.isEmpty ?? true)) continue;
|
||||
_add(
|
||||
name,
|
||||
AdTabConfig(
|
||||
bannerAds: banners,
|
||||
shuAds: apps != null ? AdGroup(items: apps, title: '热门推荐') : null,
|
||||
));
|
||||
}
|
||||
|
||||
// tab 数量随数据变化,controller 要跟着重建
|
||||
tabCtr?.dispose();
|
||||
tabCtr = TabController(length: tabs.length, vsync: this);
|
||||
}
|
||||
|
||||
void _add(String name, AdTabConfig config) {
|
||||
tabs.add(name);
|
||||
configs.add(config);
|
||||
}
|
||||
|
||||
/// 取该分类下的 banner 广告
|
||||
List<AdsInfoModel> _banners(int moduleType) =>
|
||||
model?.adv?.where((it) => it.moduleType == moduleType).toList() ?? [];
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import 'lou_feng_ad_logic.dart';
|
||||
import 'widget/recommend_app_page.dart';
|
||||
|
||||
/// 应用推荐页,可从广告链接再次进入(taskhall),故用 uniqueTag 隔离多实例
|
||||
class LouFengAdPage extends StatefulWidget {
|
||||
const LouFengAdPage({super.key});
|
||||
|
||||
@override
|
||||
State<LouFengAdPage> createState() => _LouFengAdPageState();
|
||||
}
|
||||
|
||||
class _LouFengAdPageState extends State<LouFengAdPage> with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<LouFengAdLogic>(
|
||||
tag: uniqueTag,
|
||||
init: LouFengAdLogic(),
|
||||
builder: (logic) {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.model == null)
|
||||
return CErrorWidget(retryOnTap: logic.loadData);
|
||||
//只有「应用」一个分类时不展示 tab
|
||||
if (logic.tabs.length <= 1) return _adView(logic, 0);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
14.sizeBoxH,
|
||||
//分类 tab:无指示器,靠文字颜色区分选中态
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 9),
|
||||
child: TabBar(
|
||||
controller: logic.tabCtr,
|
||||
padding: EdgeInsets.zero,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
labelStyle: TextStyle(fontSize: 14, color: Color(0xE5FFFFFF)),
|
||||
unselectedLabelStyle:
|
||||
TextStyle(fontSize: 14, color: Color(0xffACBABF)),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorColor: Color(0x00FFFFFF),
|
||||
indicatorWeight: 1,
|
||||
labelColor: Color(0xE5FFFFFF),
|
||||
labelPadding: EdgeInsets.zero,
|
||||
tabs: logic.tabs
|
||||
.map((it) => Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
child: Text(it),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: logic.tabCtr,
|
||||
children: List.generate(
|
||||
logic.configs.length, (i) => _adView(logic, i).keepAlive),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _adView(LouFengAdLogic logic, int index) =>
|
||||
RecommendAppPage(logic.configs[index]);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../config/config.dart';
|
||||
import '../mine_share/mine_share_record_logic.dart';
|
||||
import '../mine_share/mine_share_record_page.dart';
|
||||
import '../widgets/mine_share_qr_view.dart';
|
||||
|
||||
class MineIncomePage extends StatefulWidget {
|
||||
const MineIncomePage({super.key});
|
||||
|
||||
@override
|
||||
State<MineIncomePage> createState() => _MineIncomePageState();
|
||||
}
|
||||
|
||||
class _MineIncomePageState extends State<MineIncomePage> with UniqueTagMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
globalStore.refreshWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MineShareRecordLogic>(
|
||||
tag: uniqueTag,
|
||||
init: MineShareRecordLogic(),
|
||||
builder: (logic) => SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 0, right: 0, top: 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset("proxy_header_bg.webp".mineImgPath,
|
||||
fit: BoxFit.fill),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(10, 23, 10, 6),
|
||||
// decoration: BoxDecoration(color: Color(0xFF030F18)),
|
||||
child: Column(
|
||||
children: [
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, store, __) => Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: store.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
globalStore.meInfo?.name ?? "",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFFFFF),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 18.0),
|
||||
),
|
||||
5.sizeBoxH,
|
||||
Text(
|
||||
"开通会员 畅享专属特权",
|
||||
style: const TextStyle(
|
||||
color: Color(0xff989898),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12),
|
||||
)
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.to(() => MineShareRecordPage());
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"邀请人数",
|
||||
style: const TextStyle(
|
||||
color: Color(0xB2FFFFFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
SizedBox(height: 1),
|
||||
Text(
|
||||
"${logic.model?.totalInviteUserCount ?? 0}",
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF68804),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 24.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
if (Config.proxyBanner?.additionalProp != null)
|
||||
Column(
|
||||
children: [
|
||||
29.sizeBoxH,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
pushToPageByLink(
|
||||
Config.proxyBanner?.additionalProp?.url ??
|
||||
"");
|
||||
},
|
||||
child: Config.proxyBanner?.additionalProp
|
||||
?.banner !=
|
||||
null
|
||||
? NetworkImageLoader(
|
||||
imageUrl: Config.proxyBanner
|
||||
?.additionalProp?.banner ??
|
||||
'',
|
||||
width: double.infinity,
|
||||
borderRadius: 0,
|
||||
)
|
||||
: Image.asset(
|
||||
'proxy_banner.webp'.mineImgPath,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
"规则说明",
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFFFFFFF),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
width: double.infinity,
|
||||
child: EasyRichText(
|
||||
'每邀请3名好友成功注册即可获得3天VIP',
|
||||
textAlign: TextAlign.left,
|
||||
defaultStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0x73FFFFFF)),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '3名好友',
|
||||
style: TextStyle(color: Color(0xFFF68804)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
width: double.infinity,
|
||||
child: EasyRichText(
|
||||
'邀请说明:点击【保存二维码】或【复制推广链接】分享给朋友下载即可',
|
||||
textAlign: TextAlign.left,
|
||||
defaultStyle:
|
||||
TextStyle(fontSize: 12, color: Color(0x73FFFFFF)),
|
||||
),
|
||||
),
|
||||
MineShareQRView(),
|
||||
12.sizeBoxH,
|
||||
Image.asset(
|
||||
"invite_steps.webp".mineImgPath,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
24.sizeBoxH,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
//免费专区排序 tab(标题与接口 sort 值绑定)
|
||||
const _sortTabs = [SortTab('最多收藏', 1), SortTab('最新上架', 2), SortTab('最多观看', 3)];
|
||||
|
||||
class MoreFreeVideoLogic extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
final String vId;
|
||||
List<String> get sortTitles => _sortTabs.map((e) => e.name).toList();
|
||||
MoreFreeVideoLogic(this.vId);
|
||||
|
||||
bool isLoading = true;
|
||||
int page = 1;
|
||||
int sort = 0;
|
||||
late final TabController tabController =
|
||||
TabController(length: _sortTabs.length, vsync: this);
|
||||
RefreshController? refreshController;
|
||||
List<VideoModel> dataSource = [];
|
||||
|
||||
fetchPageData({bool isRefresh = true, bool showLoading = false}) async {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
if (showLoading) {
|
||||
isLoading = true;
|
||||
update();
|
||||
}
|
||||
final res = await VidService.fetchFreeSourceList(vId,
|
||||
page: page, sortType: _sortTabs[sort].sort);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.list ?? []);
|
||||
page += 1;
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../home/home_cell_style/video_simple_cell.dart';
|
||||
import 'more_free_video_logic.dart';
|
||||
|
||||
class MoreFreeVideoPage extends StatelessWidget {
|
||||
final String title;
|
||||
final String vId;
|
||||
const MoreFreeVideoPage(this.title, this.vId, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MoreFreeVideoLogic>(
|
||||
init: MoreFreeVideoLogic(vId),
|
||||
builder: (controller) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Container(
|
||||
width: 182,
|
||||
height: 31,
|
||||
padding: EdgeInsets.all(2),
|
||||
decoration: ShapeDecoration(
|
||||
shape: StadiumBorder(),
|
||||
color: Color(0xFF20252F),
|
||||
),
|
||||
child: TabBar(
|
||||
tabAlignment: TabAlignment.fill,
|
||||
tabs: controller.sortTitles.map((e) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(e),
|
||||
);
|
||||
}).toList(),
|
||||
labelStyle: TextStyle(
|
||||
color: Color(0xFFF1F3F4),
|
||||
fontSize: 12,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Color(0xFFACBABF),
|
||||
fontSize: 12,
|
||||
),
|
||||
controller: controller.tabController,
|
||||
padding: EdgeInsets.zero,
|
||||
isScrollable: false,
|
||||
onTap: (index) {
|
||||
controller.sort = index;
|
||||
controller.fetchPageData(showLoading: true);
|
||||
},
|
||||
labelPadding: EdgeInsets.zero,
|
||||
indicator: ShapeDecoration(
|
||||
shape: StadiumBorder(),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(0xFF83A4F8), Color(0xFF2A5CDE)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => controller.refreshController = ctr,
|
||||
onRefresh: (_) => controller.fetchPageData(),
|
||||
onLoading: (_) => controller.fetchPageData(isRefresh: false),
|
||||
child: () {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
if (controller.dataSource.isEmpty) return CErrorWidget();
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 7,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 191 / 174,
|
||||
),
|
||||
itemCount: controller.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: VideoSimpleCell(
|
||||
videoModel: controller.dataSource[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/user/user_income_info_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
|
||||
import '../make_money/withdrawal_page.dart';
|
||||
import 'share_details_page.dart';
|
||||
|
||||
class IncomeData {
|
||||
String value;
|
||||
String title;
|
||||
|
||||
IncomeData(this.title, this.value);
|
||||
}
|
||||
|
||||
class ShareDataListPage extends StatefulWidget {
|
||||
const ShareDataListPage({super.key});
|
||||
|
||||
@override
|
||||
State<ShareDataListPage> createState() => _ShareDataListPageState();
|
||||
}
|
||||
|
||||
class _ShareDataListPageState extends State<ShareDataListPage> {
|
||||
UserIncomeModel? _model;
|
||||
bool isLoading = true;
|
||||
final incomeList = <IncomeData>[];
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
final res = await MineService.fetchIncomeInfo();
|
||||
isLoading = false;
|
||||
_model = res;
|
||||
incomeList.add(IncomeData("当月收益(元)", _model?.monthIncomeAmount ?? '0'));
|
||||
incomeList.add(IncomeData("当月推广数", _model?.monthInviteUserCount ?? '0'));
|
||||
incomeList.add(IncomeData("今日收益(元)", _model?.todayIncomeAmount ?? '0'));
|
||||
incomeList.add(IncomeData("今日推广数", _model?.todayInviteUserCount ?? '0'));
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('推广数据'),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.to(() => ShareDetailsPage(model: _model)),
|
||||
child: Text(
|
||||
'收益明细',
|
||||
style: TextStyle(color: Color(0xFF989898), fontSize: 12),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW
|
||||
],
|
||||
),
|
||||
body: () {
|
||||
if (isLoading) return LoadingCenterWidget();
|
||||
if (_model == null) return CErrorWidget();
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border:
|
||||
Border.all(color: const Color(0xff1e262e), width: 1),
|
||||
color: Color(0xFF131b23)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"钱包余额",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffb8bbbd),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
Text(
|
||||
"${_model?.totalAmount}",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 24.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 57,
|
||||
width: .5,
|
||||
child: DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: Color(0xff444444))),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"累积收益",
|
||||
style: const TextStyle(
|
||||
color: Color(0xffb8bbbd),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
Text(
|
||||
"${_model?.totalIncomeAmount}",
|
||||
style: const TextStyle(
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 24.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(WithdrawalPage()),
|
||||
child: Container(
|
||||
width: 276,
|
||||
height: 36,
|
||||
margin: EdgeInsets.symmetric(vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(18)),
|
||||
color: Color(0xFFcf452f),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"立即提现",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
border:
|
||||
Border.all(color: const Color(0xff1e262e), width: 1),
|
||||
color: Color(0xFF131b23),
|
||||
),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 5 / 3,
|
||||
children:
|
||||
incomeList.map((e) => _buildIncomeGItem(e)).toList(),
|
||||
),
|
||||
),
|
||||
15.sizeBoxH,
|
||||
Text(
|
||||
"推广总统计",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
15.sizeBoxH,
|
||||
_buildTGItem("累计推广用户", _model?.totalInviteUserCount ?? '0'),
|
||||
_buildTGItem("累计付费用户", _model?.totalPayUserCount ?? '0'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}());
|
||||
}
|
||||
|
||||
Widget _buildTGItem(String title, String value) {
|
||||
return Container(
|
||||
height: 35,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffb8bbbd),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
),
|
||||
Text(
|
||||
"$value",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIncomeGItem(IncomeData item) {
|
||||
return Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${item.value}",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18.0),
|
||||
),
|
||||
Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffb8bbbd),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12.0),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/user/user_income_info_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import 'share_data_list_page.dart';
|
||||
|
||||
class ShareDetailsPage extends StatefulWidget {
|
||||
final UserIncomeModel? model;
|
||||
|
||||
const ShareDetailsPage({super.key, this.model});
|
||||
|
||||
@override
|
||||
State<ShareDetailsPage> createState() => _ShareDetailsPageState();
|
||||
}
|
||||
|
||||
class _ShareDetailsPageState extends State<ShareDetailsPage> {
|
||||
RefreshController? _controller;
|
||||
final incomeList = <IncomeData>[];
|
||||
int page = 1;
|
||||
bool isLoading = true;
|
||||
final dataSource = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final model = widget.model;
|
||||
incomeList.add(IncomeData("总推广人数", model?.totalInviteUserCount ?? '0'));
|
||||
incomeList.add(IncomeData("总推广收益", model?.totalIncomeAmount ?? '0'));
|
||||
incomeList.add(IncomeData("今日推广人数", model?.todayInviteUserCount ?? '0'));
|
||||
incomeList.add(IncomeData("今日推广收益", model?.todayIncomeAmount ?? '0'));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_fetchPageData();
|
||||
});
|
||||
}
|
||||
|
||||
_fetchPageData({bool isRefresh = true}) async {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
final res = await MineService.fetchIncomeList(page: 1);
|
||||
isLoading = false;
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
}
|
||||
res?.hasNext ?? false
|
||||
? _controller?.loadComplete()
|
||||
: _controller?.loadNoData();
|
||||
dataSource.addAll(res?.items ?? []);
|
||||
page += 1;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('推广数据'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: pullYsRefresh(
|
||||
onLoading: (_) => _fetchPageData(isRefresh: false),
|
||||
onRefresh: (_) => _fetchPageData(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
itemCount: incomeList.length,
|
||||
itemBuilder: (_, index) => _buildIncomeItem(incomeList[index]),
|
||||
separatorBuilder: (_, index) => 10.sizeBoxH,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 10),
|
||||
// 收益明细
|
||||
Text(
|
||||
"收益明细",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffbec4d6),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18.0),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
() {
|
||||
if (isLoading)
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: LoadingCenterWidget(),
|
||||
),
|
||||
);
|
||||
if (dataSource.isEmpty)
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(height: 300, child: CErrorWidget()));
|
||||
return SliverList.builder(
|
||||
itemCount: dataSource.length,
|
||||
itemBuilder: (_, index) {
|
||||
final item = dataSource[index];
|
||||
return _buildRecordItem(item.userName, item.incomeAmount);
|
||||
});
|
||||
}()
|
||||
],
|
||||
),
|
||||
onInit: (_) => _controller = _,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordItem(String title, int value) {
|
||||
return Container(
|
||||
height: 35,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffffffff),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.0),
|
||||
),
|
||||
Text(
|
||||
"收益+$value元",
|
||||
style: const TextStyle(
|
||||
color: const Color(0xffF68804),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIncomeItem(IncomeData item) {
|
||||
return Container(
|
||||
height: 65,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(4)),
|
||||
border: Border.all(color: const Color(0xff1e262e), width: 1),
|
||||
color: Color(0xFF131b23),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
color: const Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14.0),
|
||||
),
|
||||
Text(
|
||||
"${item.value}",
|
||||
style: const TextStyle(
|
||||
color: const Color(0x8CFFFFFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../hj_model/mine/task_center_data.dart';
|
||||
import '../../../hj_utils/api_service/common_service.dart';
|
||||
import '../../../hj_utils/api_service/mine_service.dart';
|
||||
|
||||
class SignDailyPageLogic extends GetxController with GetTickerProviderStateMixin {
|
||||
int shareCount = 0;
|
||||
List<DailyTask>? taskList;
|
||||
List<DailyTask>? dailyTask;
|
||||
final outerCtr = ScrollController();
|
||||
final tabKey = GlobalKey();
|
||||
String signBackgroundImage = '';
|
||||
|
||||
final tabs = <String>["福利任务", "积分兑换"];
|
||||
|
||||
late final TabController tabCtr = TabController(
|
||||
initialIndex: 0,
|
||||
length: tabs.length,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
/// AppBar 背景透明度,由滚动驱动:0.0 透明 → 1.0 不透明
|
||||
double appbarOpacity = 0.0;
|
||||
|
||||
/// 滚动多少像素后 AppBar 完全不透明
|
||||
static const double _kAppBarFadeMaxOffset = 100.0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
outerCtr.addListener(_onOuterScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
outerCtr.removeListener(_onOuterScroll);
|
||||
outerCtr.dispose();
|
||||
tabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 监听外层滚动,按偏移量更新 AppBar 透明度并局部刷新
|
||||
void _onOuterScroll() {
|
||||
final newOpacity = (outerCtr.offset / _kAppBarFadeMaxOffset).clamp(0.0, 1.0);
|
||||
if ((newOpacity - appbarOpacity).abs() < 0.01) return;
|
||||
appbarOpacity = newOpacity;
|
||||
update(['appbar']);
|
||||
}
|
||||
|
||||
///初始化任务列表
|
||||
Future<void> loadData() async {
|
||||
final signResult = await MineService.getSignList();
|
||||
signBackgroundImage = signResult?.config?.backgroundImage ?? '';
|
||||
|
||||
final result = await CommonService.getTaskList();
|
||||
taskList ??= [];
|
||||
taskList?.clear();
|
||||
|
||||
result?.dailyTask?.forEach((element) {
|
||||
element.doType = 1;
|
||||
});
|
||||
taskList?.addAll(result?.dailyTask ?? []);
|
||||
|
||||
result?.growthTasks?.forEach((element) {
|
||||
element.doType = 3;
|
||||
});
|
||||
taskList?.addAll(result?.growthTasks ?? []);
|
||||
|
||||
result?.onceTask?.forEach((element) {
|
||||
element.doType = 2;
|
||||
});
|
||||
taskList?.addAll(result?.onceTask ?? []);
|
||||
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/mine/widgets/integral_exchange_page.dart';
|
||||
import 'package:hgdj/hj_utils/free_play_manager.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../tools_base/global_store/store.dart';
|
||||
import '../../../tools_base/indicator/custom_tab_indicator.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import '../../main_page/provider/msg_provider.dart';
|
||||
import '../../pre_sale/pre_sale_provider.dart';
|
||||
import '../mine_vip/mine_charge_vip_page.dart';
|
||||
import '../widgets/exchange_vip_page.dart';
|
||||
import 'sign_daily_logic.dart';
|
||||
import 'widget/task_widgets.dart';
|
||||
|
||||
//每日签到
|
||||
class SignDailyPage extends StatelessWidget {
|
||||
const SignDailyPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<SignDailyPageLogic>(
|
||||
init: SignDailyPageLogic(),
|
||||
global: false,
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: _buildAppBar(logic),
|
||||
body: _buildBody(logic),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// AppBar:背景透明度跟随滚动变化(局部刷新 id='appbar')
|
||||
PreferredSizeWidget _buildAppBar(SignDailyPageLogic logic) {
|
||||
return PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: GetBuilder<SignDailyPageLogic>(
|
||||
init: logic,
|
||||
global: false,
|
||||
id: 'appbar',
|
||||
builder: (_) => AppBar(
|
||||
title: const Text('每日签到'),
|
||||
backgroundColor:
|
||||
const Color(0xff030F18).withValues(alpha: logic.appbarOpacity),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(SignDailyPageLogic logic) {
|
||||
if (logic.taskList == null) return const LoadingCenterWidget();
|
||||
if (logic.taskList!.isEmpty) return const CErrorWidget();
|
||||
|
||||
return ExtendedNestedScrollView(
|
||||
controller: logic.outerCtr,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
onlyOneScrollInBody: true,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||
return [
|
||||
CupertinoSliverRefreshControl(
|
||||
onRefresh: () async {
|
||||
await logic.loadData();
|
||||
},
|
||||
),
|
||||
SliverToBoxAdapter(child: _buildVIPNewGuide(logic)),
|
||||
SliverToBoxAdapter(child: MineSignView(logic: logic)),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
key: logic.tabKey,
|
||||
margin: const EdgeInsets.fromLTRB(16, 15, 0, 6),
|
||||
child: _buildTab(logic),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'我的积分 ',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
Consumer<GlobalStore>(
|
||||
builder: (_, provider, __) => Text(
|
||||
'${globalStore.wallet?.integral ?? 0}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffF68804),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
body: TabBarView(
|
||||
controller: logic.tabCtr,
|
||||
children: [
|
||||
MineTaskView(logic.taskList!).keepAlive,
|
||||
const IntegralExchangePage().keepAlive,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_buildTab(SignDailyPageLogic logic) {
|
||||
return Row(
|
||||
children: [
|
||||
TabBar(
|
||||
padding: EdgeInsets.zero,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xffF68804),
|
||||
fontWeight: FontWeight.w500),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color(0xff999999),
|
||||
),
|
||||
controller: logic.tabCtr,
|
||||
tabs: logic.tabs.map((it) => Text(it)).toList(),
|
||||
labelPadding: const EdgeInsets.fromLTRB(0, 0, 20, 6),
|
||||
indicator: CustomIndicator(
|
||||
width: 18,
|
||||
height: 4,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(2)),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => Get.to(ExchangeVipPage()),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: Text(
|
||||
'兑换记录',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF999999),
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildVIPNewGuide(SignDailyPageLogic logic) {
|
||||
return Stack(
|
||||
children: [
|
||||
logic.signBackgroundImage.isNotEmpty
|
||||
? NetworkImageLoader(
|
||||
width: double.infinity,
|
||||
height: 240,
|
||||
fit: BoxFit.cover,
|
||||
imageUrl: logic.signBackgroundImage,
|
||||
)
|
||||
: Image.asset(
|
||||
"welfare_vip_background.webp".mineImgPath,
|
||||
width: double.infinity,
|
||||
height: 240,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
110.sizeBoxH,
|
||||
Consumer<MineMsgProvider>(
|
||||
builder: (context, provider, child) => Container(
|
||||
margin: const EdgeInsets.only(left: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: globalStore.meInfo?.portrait ?? '',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
globalStore.meInfo?.name ?? "",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
if ((globalStore.meInfo?.vipLevel ?? 0) > 0) ...[
|
||||
4.sizeBoxW,
|
||||
Image.asset(
|
||||
globalStore.meInfo?.vipImageName ?? "",
|
||||
width: 30,
|
||||
height: 18),
|
||||
],
|
||||
],
|
||||
),
|
||||
5.sizeBoxH,
|
||||
Consumer<PreSaleProvider>(
|
||||
builder: (context, provider, child) {
|
||||
final isVip = globalStore.isVIP;
|
||||
final watch =
|
||||
FreePlayManager().remain?.watchCount ?? 0;
|
||||
final aiFree =
|
||||
globalStore.wallet?.aiUndressFreeTimes ?? 0;
|
||||
final aiToday =
|
||||
provider.remain?.todayAiUndressCount ?? 0;
|
||||
return Text(
|
||||
isVip
|
||||
? "剩余AI次数:${aiFree + aiToday}"
|
||||
: "剩余观看次数:$watch",
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: Color(0xff656565)),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
22.sizeBoxH,
|
||||
if (!globalStore.isVIP)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Get.to(MineChargeVipPage());
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.h),
|
||||
margin: EdgeInsets.symmetric(horizontal: 40.w, vertical: 0),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(90)),
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFFFF9077), Color(0xFFFF6E4E)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
globalStore.isVIP ? '已开通会员' : '开通VIP不限次数免费观看',
|
||||
style: TextStyle(
|
||||
color: Color(
|
||||
globalStore.isVIP ? 0xfffffffff : 0xfffffffff),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (globalStore.isVIP) TaskCenterVIPGuideView(),
|
||||
20.sizeBoxW,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import '../../../tools_base/indicator/custom_tab_indicator.dart';
|
||||
import 'lou_feng_ad_page.dart';
|
||||
import 'mine_income_page.dart';
|
||||
|
||||
class WelfareHomePage extends StatefulWidget {
|
||||
///默认选中的 tab:0 分享邀请 / 1 应用推荐
|
||||
final int index;
|
||||
|
||||
const WelfareHomePage({super.key, this.index = 0});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _WelfareHomePageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _WelfareHomePageState extends State<WelfareHomePage>
|
||||
with TickerProviderStateMixin {
|
||||
final tabs = <String>['分享邀请', "应用推荐"];
|
||||
late final TabController _tabController = TabController(
|
||||
initialIndex: widget.index, length: tabs.length, vsync: this);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false, // 不自动显示返回按钮
|
||||
toolbarHeight: 56, // 高度保持默认
|
||||
titleSpacing: 0, // 去除左右空隙
|
||||
title: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back_ios),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0, vertical: 5),
|
||||
isScrollable: false,
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xE5FFFFFF),
|
||||
fontWeight: FontWeight.w400),
|
||||
unselectedLabelStyle:
|
||||
TextStyle(fontSize: 14, color: Color(0x73FFFFFF)),
|
||||
indicator: CustomIndicator(
|
||||
width: 18,
|
||||
height: 4,
|
||||
isGradient: true,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(0),
|
||||
bottomRight: Radius.circular(2),
|
||||
),
|
||||
gradientColors: const [Color(0x55F68804), Color(0xffF68804)],
|
||||
offsetY: -8,
|
||||
),
|
||||
controller: _tabController,
|
||||
tabs: tabs.map((it) => Text(it)).toList(),
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 54),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
MineIncomePage().keepAlive,
|
||||
LouFengAdPage().keepAlive,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/// 签到列表响应模型
|
||||
class CheckinPrizeResp {
|
||||
CheckinInfo? checkin;
|
||||
CheckinConfig? config;
|
||||
List<CheckinPrize>? prizes;
|
||||
List<CheckinPrize>? bigPrizes;
|
||||
|
||||
CheckinPrizeResp({this.checkin, this.config, this.prizes});
|
||||
|
||||
CheckinPrizeResp.fromJson(Map<String, dynamic> json) {
|
||||
checkin = json['checkin'] != null
|
||||
? CheckinInfo.fromJson(json['checkin'])
|
||||
: null;
|
||||
config = json['config'] != null
|
||||
? CheckinConfig.fromJson(json['config'])
|
||||
: null;
|
||||
if (json['prizes'] != null) {
|
||||
prizes = <CheckinPrize>[];
|
||||
json['prizes'].forEach((v) {
|
||||
prizes!.add(CheckinPrize.fromJson(v));
|
||||
});
|
||||
}
|
||||
if (json['bigPrizes'] != null) {
|
||||
bigPrizes = <CheckinPrize>[];
|
||||
json['bigPrizes'].forEach((v) {
|
||||
bigPrizes!.add(CheckinPrize.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
if (checkin != null) {
|
||||
data['checkin'] = checkin!.toJson();
|
||||
}
|
||||
if (config != null) {
|
||||
data['config'] = config!.toJson();
|
||||
}
|
||||
if (prizes != null) {
|
||||
data['prizes'] = prizes!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
if (bigPrizes != null) {
|
||||
data['bigPrizes'] = bigPrizes!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 签到信息
|
||||
class CheckinInfo {
|
||||
int? continuouslyDays; // 连续签到天数
|
||||
int? cumulativeDays; // 累计签到天数(本月)
|
||||
bool? todayChecked; // 今日是否已经签过到
|
||||
bool? doubleReward;
|
||||
CheckinInfo({this.continuouslyDays, this.cumulativeDays, this.todayChecked,this.doubleReward});
|
||||
|
||||
CheckinInfo.fromJson(Map<String, dynamic> json) {
|
||||
continuouslyDays = json['continuouslyDays'];
|
||||
cumulativeDays = json['cumulativeDays'];
|
||||
todayChecked = json['todayChecked'];
|
||||
doubleReward=json['doubleReward'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['continuouslyDays'] = continuouslyDays;
|
||||
data['cumulativeDays'] = cumulativeDays;
|
||||
data['todayChecked'] = todayChecked;
|
||||
data['doubleReward']=doubleReward;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 签到配置
|
||||
class CheckinConfig {
|
||||
String? backgroundImage; // 背景图片
|
||||
String? description; // 规则说明
|
||||
bool? enable; // 是否启用
|
||||
String? rewardBgVideoUrl; //视频
|
||||
List<IntegerExchange>? integerExchangeList;
|
||||
|
||||
CheckinConfig({this.backgroundImage, this.description, this.enable,this.rewardBgVideoUrl});
|
||||
|
||||
CheckinConfig.fromJson(Map<String, dynamic> json) {
|
||||
backgroundImage = json['backgroundImage'];
|
||||
description = json['description'];
|
||||
enable = json['enable'];
|
||||
rewardBgVideoUrl = json['rewardBgVideoUrl'];
|
||||
if (json['integerExchangeList'] != null) {
|
||||
integerExchangeList = <IntegerExchange>[];
|
||||
json['integerExchangeList'].forEach((v) {
|
||||
integerExchangeList!.add(IntegerExchange.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['backgroundImage'] = backgroundImage;
|
||||
data['description'] = description;
|
||||
data['enable'] = enable;
|
||||
data['rewardBgVideoUrl'] = rewardBgVideoUrl;
|
||||
if (integerExchangeList != null) {
|
||||
data['integerExchangeList'] = integerExchangeList!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IntegerExchange{
|
||||
String? name;
|
||||
String? icon;
|
||||
IntegerExchange({this.name, this.icon});
|
||||
IntegerExchange.fromJson(Map<String, dynamic> json) {
|
||||
name = json['name'];
|
||||
icon = json['icon'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['name'] = name;
|
||||
data['icon'] = icon;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
/// 签到奖励
|
||||
class CheckinPrize {
|
||||
bool? bigPrize; // 大奖,有问题提示的
|
||||
int? checkinDays; // 签到天数
|
||||
int? checkinType; // 签到类型 Enum: 0, 1, 2
|
||||
String? createdAt;
|
||||
String? id; // ID
|
||||
String? image; // 图片
|
||||
String? prizeId; // 奖品ID
|
||||
bool? status; // 状态(是否已领取)
|
||||
String? title; // 标题
|
||||
String? updatedAt;
|
||||
int? score; // 签到奖励积分数量
|
||||
String? prizeName;
|
||||
bool? isCheckedIn;
|
||||
bool? canClaim;//是否可以补领
|
||||
bool? isReceive;//是否可以补领
|
||||
bool? isExpired;//是否过期
|
||||
|
||||
CheckinPrize({
|
||||
this.bigPrize,
|
||||
this.checkinDays,
|
||||
this.checkinType,
|
||||
this.createdAt,
|
||||
this.id,
|
||||
this.image,
|
||||
this.prizeId,
|
||||
this.status,
|
||||
this.title,
|
||||
this.updatedAt,
|
||||
this.score,
|
||||
this.prizeName,
|
||||
this.isCheckedIn,
|
||||
this.isReceive,
|
||||
this.canClaim,
|
||||
this.isExpired,
|
||||
});
|
||||
|
||||
CheckinPrize.fromJson(Map<String, dynamic> json) {
|
||||
bigPrize = json['bigPrize'];
|
||||
checkinDays = json['checkinDays'];
|
||||
checkinType = json['checkinType'];
|
||||
createdAt = json['createdAt'];
|
||||
id = json['id'];
|
||||
image = json['image'];
|
||||
prizeId = json['prizeId'];
|
||||
status = json['status'];
|
||||
title = json['title'];
|
||||
updatedAt = json['updatedAt'];
|
||||
score = json['score'];
|
||||
prizeName = json['prizeName'];
|
||||
isCheckedIn = json['isCheckedIn'];
|
||||
canClaim = json['canClaim'];
|
||||
isReceive = json['isReceive'];
|
||||
isExpired = json['isExpired'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['bigPrize'] = bigPrize;
|
||||
data['checkinDays'] = checkinDays;
|
||||
data['checkinType'] = checkinType;
|
||||
data['createdAt'] = createdAt;
|
||||
data['id'] = id;
|
||||
data['image'] = image;
|
||||
data['prizeId'] = prizeId;
|
||||
data['status'] = status;
|
||||
data['title'] = title;
|
||||
data['updatedAt'] = updatedAt;
|
||||
data['score'] = score;
|
||||
data['prizeName'] = prizeName;
|
||||
data['isCheckedIn'] = isCheckedIn;
|
||||
data['canClaim'] = canClaim;
|
||||
data['isReceive'] = isReceive;
|
||||
data['isExpired'] = isExpired;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行签到响应
|
||||
class CheckinDoResp {
|
||||
CheckinInfo? checkin;
|
||||
String? message;
|
||||
List<CheckinRewardPrize>? prizes;
|
||||
String? prizeVideo;
|
||||
|
||||
CheckinDoResp({this.checkin, this.message, this.prizes});
|
||||
|
||||
CheckinDoResp.fromJson(Map<String, dynamic> json) {
|
||||
checkin = json['checkin'] != null
|
||||
? CheckinInfo.fromJson(json['checkin'])
|
||||
: null;
|
||||
message = json['message'];
|
||||
prizeVideo = json['prizeVideo'];
|
||||
if (json['prizes'] != null) {
|
||||
prizes = <CheckinRewardPrize>[];
|
||||
json['prizes'].forEach((v) {
|
||||
prizes!.add(CheckinRewardPrize.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
if (checkin != null) {
|
||||
data['checkin'] = checkin!.toJson();
|
||||
}
|
||||
data['message'] = message;
|
||||
data['prizeVideo'] = prizeVideo;
|
||||
if (prizes != null) {
|
||||
data['prizes'] = prizes!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 签到奖励详情
|
||||
class CheckinRewardPrize {
|
||||
bool? countRand;
|
||||
int? prizeCount;
|
||||
String? prizeImage;
|
||||
String? prizeTitle;
|
||||
int? prizeType;
|
||||
|
||||
CheckinRewardPrize({
|
||||
this.countRand,
|
||||
this.prizeCount,
|
||||
this.prizeImage,
|
||||
this.prizeTitle,
|
||||
this.prizeType,
|
||||
});
|
||||
|
||||
CheckinRewardPrize.fromJson(Map<String, dynamic> json) {
|
||||
countRand = json['countRand'];
|
||||
prizeCount = json['prizeCount'];
|
||||
prizeImage = json['prizeImage'];
|
||||
prizeTitle = json['prizeTitle'];
|
||||
prizeType = json['prizeType'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {};
|
||||
data['countRand'] = countRand;
|
||||
data['prizeCount'] = prizeCount;
|
||||
data['prizeImage'] = prizeImage;
|
||||
data['prizeTitle'] = prizeTitle;
|
||||
data['prizeType'] = prizeType;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'checkin_model.dart';
|
||||
|
||||
class CongratulationsRewardDialog extends StatelessWidget {
|
||||
final VoidCallback? onExchange;
|
||||
final List<IntegerExchange>? prizeList;
|
||||
// final String? videoUrl;
|
||||
// final String? videoToken;
|
||||
final VideoPlayerController? videoPlayerCtr;
|
||||
const CongratulationsRewardDialog({
|
||||
super.key,
|
||||
this.onExchange,
|
||||
this.prizeList,
|
||||
// this.videoUrl,
|
||||
// this.videoToken,
|
||||
this.videoPlayerCtr,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 不包 Dialog / 不铺满屏幕 → 点击 content 之外的区域由 Get.dialog 的 barrierDismissible 关闭
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 27),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 356,
|
||||
width: 319,
|
||||
child: VideoPlayer(videoPlayerCtr!),
|
||||
),
|
||||
SizedBox(height: 18),
|
||||
Text(
|
||||
"- 积分可兑换以下豪礼 -",
|
||||
style: TextStyle(color: Color(0xffFFD900), fontSize: 14),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
// 奖励列表
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: prizeList
|
||||
?.map((e) => _buildRewardItem(e.name ?? "", e.icon ?? ""))
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
SizedBox(height: 36),
|
||||
// 底部按钮
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
onExchange?.call();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD700), width: 2),
|
||||
),
|
||||
child: Text(
|
||||
"兑换好礼",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFD700),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFFD900),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Text(
|
||||
"继续领积分",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 33),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewardItem(String title, String iconPath) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 53,
|
||||
height: 53,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: .5), width: 0.5)),
|
||||
padding: EdgeInsets.all(6),
|
||||
child: NetworkImageLoader(imageUrl: iconPath, fit: BoxFit.contain),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: 65,
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
class CustomProgressBar extends StatelessWidget {
|
||||
final double progress; // 0 ~ 1
|
||||
|
||||
const CustomProgressBar({super.key, required this.progress});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
double width = constraints.maxWidth;
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
SizedBox(height: 18),
|
||||
// 背景
|
||||
Container(
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
|
||||
// 进度
|
||||
Container(
|
||||
height: 6,
|
||||
width: width * progress,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Color(0xffF68804),
|
||||
),
|
||||
),
|
||||
// 小图标(跟随进度,两端不溢出)
|
||||
Positioned(
|
||||
left: (width - 18) * progress,
|
||||
child: Image.asset("ic_sign_indicator.webp".mineImgPath,
|
||||
width: 18, height: 18, fit: BoxFit.cover),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
8.sizeBoxW,
|
||||
// 右侧百分比
|
||||
Text(
|
||||
"${(progress * 100).toInt()}%",
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/address.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_share/mine_share_page.dart';
|
||||
import 'package:hgdj/hj_page/mine/welfare/sign_daily_logic.dart';
|
||||
import 'package:hgdj/hj_page/mine/welfare/widget/checkin_model.dart';
|
||||
import 'package:hgdj/hj_page/mine/welfare/widget/congratulations_reward_dialog.dart';
|
||||
import 'package:hgdj/hj_page/mine/welfare/widget/sign_dialog.dart';
|
||||
import 'package:hgdj/hj_page/mine/welfare/widget/sign_in_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:hgdj/hj_utils/video_view_type.dart';
|
||||
|
||||
/// 每日签到 Logic
|
||||
/// 负责签到列表加载、签到/补领接口、双倍奖励视频播放、跳转积分兑换 Tab
|
||||
class MineSignLogic extends GetxController {
|
||||
/// 父级页面 Logic(用于切 Tab + 滚动到 Tab 位置)
|
||||
final SignDailyPageLogic? parentLogic;
|
||||
MineSignLogic({this.parentLogic});
|
||||
|
||||
// ========== 状态 ==========
|
||||
bool isSigned = false; // 今日是否签到
|
||||
bool doubleReward = false; // 是否触发双倍奖励
|
||||
int continuousDays = 0; // 连续签到天数
|
||||
int cumulativeDays = 0; // 累计签到天数(本月)
|
||||
String? videoUrl; // 双倍奖励视频 ID
|
||||
List<CheckinPrize>? checkList; // 普通签到奖励
|
||||
List<CheckinPrize>? vipCheckList; // 会员签到奖励(大礼)
|
||||
List<IntegerExchange>? prizeLists; // 积分兑换列表
|
||||
final extraRewardList = <ExtraSignRewardItem>[]; // 额外签到奖励
|
||||
|
||||
// ========== 滚动控制器(三排联动) ==========
|
||||
late final LinkedScrollControllerGroup scrollGroup;
|
||||
late final ScrollController titleScrollCtr; // 第 0 排:天数标题
|
||||
late final ScrollController normalScrollCtr; // 第 1 排:普通奖励
|
||||
late final ScrollController vipScrollCtr; // 第 2 排:会员奖励
|
||||
VideoPlayerController? videoPlayerCtr;
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
scrollGroup = LinkedScrollControllerGroup();
|
||||
titleScrollCtr = scrollGroup.addAndGet();
|
||||
normalScrollCtr = scrollGroup.addAndGet();
|
||||
vipScrollCtr = scrollGroup.addAndGet();
|
||||
getSignList();
|
||||
getExtralSignList();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
titleScrollCtr.dispose();
|
||||
normalScrollCtr.dispose();
|
||||
vipScrollCtr.dispose();
|
||||
videoPlayerCtr?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ========== 公开方法 ==========
|
||||
|
||||
/// 获取签到主列表(普通 + 会员奖励 + 积分兑换配置)
|
||||
Future<void> getSignList() async {
|
||||
final res = await MineService.getSignList();
|
||||
if (res == null) return;
|
||||
isSigned = res.checkin?.todayChecked ?? false;
|
||||
continuousDays = res.checkin?.continuouslyDays ?? 0;
|
||||
cumulativeDays = res.checkin?.cumulativeDays ?? 0;
|
||||
doubleReward = res.checkin?.doubleReward ?? false;
|
||||
prizeLists = res.config?.integerExchangeList;
|
||||
checkList = res.prizes;
|
||||
vipCheckList = res.bigPrizes;
|
||||
update();
|
||||
}
|
||||
|
||||
/// 获取额外签到奖励列表
|
||||
Future<void> getExtralSignList() async {
|
||||
try {
|
||||
final res = await MineService.getExtraDayMark();
|
||||
if (res != null) {
|
||||
extraRewardList
|
||||
..clear()
|
||||
..addAll(res);
|
||||
update();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
/// 点击签到按钮
|
||||
Future<void> doSignIn() async {
|
||||
if (isSigned) {
|
||||
showToast('今日已签到');
|
||||
return;
|
||||
}
|
||||
final res = await MineService.postDayMark();
|
||||
if (res?.checkin == null) return;
|
||||
showToast('签到成功');
|
||||
// 同步签到状态
|
||||
continuousDays = res!.checkin!.continuouslyDays ?? 0;
|
||||
cumulativeDays = res.checkin!.cumulativeDays ?? 0;
|
||||
isSigned = res.checkin!.todayChecked ?? false;
|
||||
doubleReward = res.checkin!.doubleReward ?? false;
|
||||
videoUrl = res.prizeVideo ?? '';
|
||||
getSignList();
|
||||
update();
|
||||
globalStore.refreshWallet();
|
||||
// 第 8 天起仅 toast 不弹窗;前 7 天:双倍奖励弹特效,否则弹普通签到弹窗
|
||||
if (continuousDays > 7) return;
|
||||
if (doubleReward) {
|
||||
_showDoubleRewardDialog();
|
||||
} else {
|
||||
_showNormalSignDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/// 会员奖励补领
|
||||
Future<void> doCheckinClaimVip() async {
|
||||
final resp = await MineService.claimCheckinVip();
|
||||
if (resp?.checkin == null) {
|
||||
showToast(resp?.message ?? '补领失败');
|
||||
return;
|
||||
}
|
||||
showToast(resp?.message ?? '补领成功');
|
||||
continuousDays = resp!.checkin!.continuouslyDays ?? 0;
|
||||
cumulativeDays = resp.checkin!.cumulativeDays ?? 0;
|
||||
isSigned = resp.checkin!.todayChecked ?? false;
|
||||
doubleReward = resp.checkin!.doubleReward ?? false;
|
||||
videoUrl = resp.prizeVideo ?? '';
|
||||
update();
|
||||
await getSignList();
|
||||
globalStore.refreshWallet();
|
||||
}
|
||||
|
||||
/// 补签:调用接口 + 弹"邀请分享"引导弹窗
|
||||
Future<void> doResign(SignInListItem item) async {
|
||||
await MineService.postReSign(item.id ?? '');
|
||||
Get.dialog(
|
||||
_ResignGuideDialog(
|
||||
onConfirm: () {
|
||||
Get.back();
|
||||
Get.to(() => MineSharePage());
|
||||
},
|
||||
onCancel: Get.back,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 切到"积分兑换" Tab,并把页面滚到 Tab 栏刚好贴在 AppBar 下方
|
||||
void gotoExchangeTab() {
|
||||
final parent = parentLogic;
|
||||
if (parent == null) return;
|
||||
parent.tabCtr.animateTo(1);
|
||||
// 等一帧让弹窗 dismiss 后 layout 稳定,再算位置
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => _scrollTabKeyBelowAppBar());
|
||||
}
|
||||
|
||||
/// 让 parent.tabKey 对应的 widget 滚到 AppBar 下沿
|
||||
void _scrollTabKeyBelowAppBar() {
|
||||
final parent = parentLogic;
|
||||
if (parent == null || !parent.outerCtr.hasClients) return;
|
||||
final ctx = parent.tabKey.currentContext;
|
||||
if (ctx == null) return;
|
||||
final box = ctx.findRenderObject() as RenderBox?;
|
||||
if (box == null || !box.attached) return;
|
||||
// tabKey widget 当前在屏幕中的 Y 坐标
|
||||
final dy = box.localToGlobal(Offset.zero).dy;
|
||||
// AppBar 总高度 = 状态栏 + 工具栏
|
||||
final appBarHeight = MediaQuery.of(ctx).padding.top + kToolbarHeight;
|
||||
// 目标偏移 = 当前偏移 + (widget 屏幕 Y - AppBar 高度)
|
||||
// 滚完后 widget 顶端正好贴 AppBar 底部
|
||||
final target = (parent.outerCtr.offset + dy - appBarHeight).clamp(
|
||||
0.0,
|
||||
parent.outerCtr.position.maxScrollExtent,
|
||||
);
|
||||
parent.outerCtr.animateTo(
|
||||
target,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
|
||||
/// 弹双倍奖励特效弹窗(视频循环播放)
|
||||
/// 弹窗关闭后会自动暂停并释放 videoPlayerCtr,避免后台仍在播放
|
||||
void _showDoubleRewardDialog() async {
|
||||
videoPlayerCtr?.dispose();
|
||||
final ctr = PlayerFactory.network(
|
||||
'${Address.baseApiPath}/vid/h5/m3u8/$videoUrl?token=${Address.token}&c=${Address.cdnAddress}');
|
||||
videoPlayerCtr = ctr;
|
||||
final dialogFuture = Get.dialog(
|
||||
CongratulationsRewardDialog(
|
||||
onExchange: gotoExchangeTab,
|
||||
prizeList: prizeLists,
|
||||
videoPlayerCtr: ctr,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await ctr.initialize();
|
||||
await ctr.setLooping(true);
|
||||
await ctr.play();
|
||||
} catch (e) {
|
||||
// 弹窗已用此 ctr 构建,无法换 view 重试,仅内存切换;落本地由后续成功播放的播放器确认
|
||||
if (isDecoderError(e)) switchToPlatformView();
|
||||
}
|
||||
// 等弹窗 dismiss(点击关闭/兑换/外部点击/back 都会触发)
|
||||
await dialogFuture;
|
||||
await ctr.pause();
|
||||
await ctr.dispose();
|
||||
// 防止与下次双倍奖励的新实例冲突
|
||||
if (identical(videoPlayerCtr, ctr)) videoPlayerCtr = null;
|
||||
}
|
||||
|
||||
/// 弹普通签到弹窗
|
||||
void _showNormalSignDialog() {
|
||||
Get.dialog(
|
||||
SignInDialog(
|
||||
signInList: checkList,
|
||||
continuousDays: continuousDays,
|
||||
prizeList: prizeLists,
|
||||
callback: gotoExchangeTab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 补签引导弹窗(私有给 Logic 用)
|
||||
class _ResignGuideDialog extends StatelessWidget {
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
const _ResignGuideDialog({required this.onConfirm, required this.onCancel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(left: 32, right: 32, bottom: 100),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 30),
|
||||
padding: const EdgeInsets.fromLTRB(24, 100, 24, 24),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('icon_resign_bg.webp'.mineImgPath),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 260),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _btn(onCancel, '离开', isPrimary: false)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _btn(onConfirm, '立即邀请', isPrimary: true)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 10,
|
||||
child: GestureDetector(
|
||||
onTap: onCancel,
|
||||
child: Image.asset('icon_resign_close.png'.mineImgPath,
|
||||
width: 30, height: 30),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _btn(VoidCallback onTap, String text, {required bool isPrimary}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 50,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: isPrimary ? null : Colors.white.withValues(alpha: 0.8),
|
||||
gradient: isPrimary
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFFF68804), Color(0xFFF68804)],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: isPrimary
|
||||
? null
|
||||
: Border.all(color: const Color(0xFFB7B7B7), width: 1),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isPrimary ? Colors.white : const Color(0xFF666666),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/mine/happy/happy_model.dart';
|
||||
import 'package:hgdj/hj_model/splash/ads_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_banner_widget.dart';
|
||||
|
||||
import '../../../../tools_base/banner/ads_item.dart';
|
||||
|
||||
class RecommendAppPage extends StatelessWidget {
|
||||
final AdTabConfig model;
|
||||
|
||||
const RecommendAppPage(this.model, {super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (model.bannerAds?.isNotEmpty ?? false)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 12),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 343 / 143,
|
||||
child: AdsBannerWidget(
|
||||
model.bannerAds ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (model.hengAds != null)
|
||||
SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _buildNormalTitle(model.hengAds?.title ?? ''),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: 10.sizeBoxH,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverGrid.builder(
|
||||
itemCount: model.hengAds?.items?.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
crossAxisSpacing: 2,
|
||||
mainAxisSpacing: 10,
|
||||
childAspectRatio: 82 / 105,
|
||||
),
|
||||
itemBuilder: (_, index) =>
|
||||
_buildVItem(model.hengAds!.items![index], index)),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (model.shuAds != null)
|
||||
SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _buildNormalTitle(model.shuAds?.title ?? ''),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
sliver: SliverList.separated(
|
||||
itemCount: model.shuAds?.items?.length ?? 0,
|
||||
separatorBuilder: (_, __) =>
|
||||
model.shuAds?.title == '热门推荐' ? 10.sizeBoxH : 1.line,
|
||||
itemBuilder: (_, index) {
|
||||
final info = model.shuAds!.items![index];
|
||||
if (model.shuAds?.title == '热门推荐')
|
||||
return _buildHBItem(info, index);
|
||||
return _buildHItem(info, index);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNormalTitle(String title) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 20, 0, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
15.sizeBoxW,
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 竖版的广告
|
||||
_buildVItem(AdsInfoModel item, int index) {
|
||||
return AdsItem(
|
||||
adInfo: item,
|
||||
clickType: 0,
|
||||
);
|
||||
}
|
||||
|
||||
_buildHItem(AdsInfoModel item, int index) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
AdsItem(
|
||||
adInfo: item,
|
||||
showType: AdShowType.hor,
|
||||
clickType: 0,
|
||||
),
|
||||
12.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildHBItem(AdsInfoModel item, int index) {
|
||||
return AdsItem(
|
||||
adInfo: item,
|
||||
showType: AdShowType.vImgText,
|
||||
clickType: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../../tools_base/widget/net_image_widget.dart';
|
||||
import 'checkin_model.dart';
|
||||
|
||||
class SignInDialog extends StatelessWidget {
|
||||
final List<CheckinPrize>? signInList; // 签到日历数据
|
||||
final int? continuousDays; // 连续签到天数
|
||||
final Function? callback;
|
||||
final List<IntegerExchange>? prizeList;
|
||||
|
||||
SignInDialog(
|
||||
{super.key,
|
||||
this.signInList,
|
||||
this.continuousDays,
|
||||
this.callback,
|
||||
this.prizeList});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
insetPadding: EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 40),
|
||||
margin: EdgeInsets.only(top: 50),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Color(0xff0F0F0F),
|
||||
border: Border.all(width: 1, color: Color(0xffFFE7B4))),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 描述
|
||||
EasyRichText(
|
||||
'已连续签到 $continuousDays 天,第7天有惊喜好礼相送!',
|
||||
defaultStyle: TextStyle(color: Colors.white, fontSize: 14),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
stringBeforeTarget: '已连续签到 ',
|
||||
targetString: '$continuousDays',
|
||||
style: TextStyle(color: Color(0xffFFDA0B), fontSize: 14),
|
||||
),
|
||||
EasyRichTextPattern(
|
||||
targetString: '第7天有惊喜好礼相送!',
|
||||
style: TextStyle(color: Color(0xffFFDA0B), fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
12.sizeBoxH,
|
||||
Divider(height: 1, color: Colors.white.withValues(alpha: .1)),
|
||||
12.sizeBoxH,
|
||||
// Grid 奖励
|
||||
_buildGrid(),
|
||||
13.sizeBoxH,
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFFD900).withValues(alpha: .1),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"- 积分可兑换以下豪礼 -",
|
||||
style: TextStyle(
|
||||
color: Color(0xffFFD900),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
// 奖励列表
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: prizeList
|
||||
?.map((e) => _buildRewardItem(
|
||||
e.name ?? "", e.icon ?? ""))
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
// 按钮
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
callback?.call();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFFD700), width: 2),
|
||||
),
|
||||
child: Text(
|
||||
"兑换好礼",
|
||||
style: TextStyle(
|
||||
color: const Color(0xFFFFD700),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.back();
|
||||
},
|
||||
child: Container(
|
||||
height: 44,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffFFD900),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Text(
|
||||
"继续领积分",
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Image.asset('sign_take_top.webp'.mineImgPath,
|
||||
height: 30, fit: BoxFit.contain)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Grid
|
||||
// =========================
|
||||
Widget _buildGrid() {
|
||||
final list = signInList?.take(6).toList() ?? [];
|
||||
return Container(
|
||||
height: 68,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(20)),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ScrollPhysics(),
|
||||
itemCount: list.length,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (_, i) {
|
||||
final item = list[i];
|
||||
return _buildItem(item, i);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// 单个格子
|
||||
// =========================
|
||||
Widget _buildItem(CheckinPrize item, int index) {
|
||||
final isSigned = (continuousDays ?? 0) > index;
|
||||
bool active = continuousDays == index;
|
||||
return Container(
|
||||
margin: EdgeInsets.only(right: 7),
|
||||
width: 42,
|
||||
height: 68,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
border: active
|
||||
? Border.all(color: const Color(0xffFFDA0B), width: 0.5)
|
||||
: null,
|
||||
),
|
||||
// padding: EdgeInsets.all(5),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
(item.image != null && item.image!.isNotEmpty)
|
||||
? ColorFiltered(
|
||||
colorFilter: isSigned == true
|
||||
? ColorFilter.matrix([
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0.2126,
|
||||
0.7152,
|
||||
0.0722,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
])
|
||||
: ColorFilter.mode(
|
||||
Colors.transparent, BlendMode.multiply),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: item.image!,
|
||||
width: 34,
|
||||
height: 34,
|
||||
fit: BoxFit.contain))
|
||||
: Image.asset(
|
||||
isSigned == true
|
||||
? 'checked_in_coin.webp'.mineImgPath
|
||||
: 'check_in_coin.webp'.mineImgPath,
|
||||
width: 34,
|
||||
height: 34,
|
||||
fit: BoxFit.cover),
|
||||
Text(
|
||||
item.prizeName ?? '',
|
||||
style: TextStyle(
|
||||
color: isSigned == true ? Colors.white : Color(0xffFFD900),
|
||||
fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRewardItem(String title, String iconPath) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 53,
|
||||
height: 53,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: .2), width: 0.5)),
|
||||
padding: EdgeInsets.all(6),
|
||||
child: NetworkImageLoader(imageUrl: iconPath, fit: BoxFit.contain),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
SizedBox(
|
||||
width: 60,
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user