初始化
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/hj_utils/light_model.dart';
|
||||
import 'package:hgdj/hj_utils/store_keys.dart';
|
||||
|
||||
import 'main_logic.dart';
|
||||
|
||||
/// 「AI爽剧来袭」引导气泡:贴底栏上沿、尾巴指着短剧 tab,原地上下轻跳。
|
||||
/// 亮出来就算数——装机维度只亮这一次(落 KV),不用等用户点;
|
||||
/// 后台把 ping 的 entryPopupEnabled 置 false 可全局关掉
|
||||
class DramaTipBubble extends StatefulWidget {
|
||||
const DramaTipBubble({super.key});
|
||||
|
||||
/// 短剧在底栏的下标。暗网排在它后面,darkWebEnable 关不关都不影响
|
||||
static const dramaTab = 1;
|
||||
|
||||
static final visible = ValueNotifier(false); // 气泡亮不亮
|
||||
|
||||
static bool _seen = false; //本次启动是否已进过短剧 tab
|
||||
|
||||
/// MainPage 创建时调一次;ping 关掉或以前亮过就不再亮
|
||||
static Future<void> start() async {
|
||||
if (!Config.entryPopupEnabled) return;
|
||||
if (await lightKV.getBool(StoreKeys.DRAMA_TIP_SHOWN) ?? false) return;
|
||||
if (_seen) return; //读盘这会儿已经进过短剧了(深链直达),这次不亮也不算数,下次冷启动照亮
|
||||
visible.value = true;
|
||||
lightKV.setBool(StoreKeys.DRAMA_TIP_SHOWN, true); //亮出来就记下,不等用户点
|
||||
}
|
||||
|
||||
/// 进短剧 tab 就收起,只管本次启动;算不算数在 [start] 亮出来那一刻就定了。
|
||||
/// 可能早于 [start] 的读盘,所以要留下 [_seen] 挡住它
|
||||
static void hide() {
|
||||
visible.value = false;
|
||||
_seen = true;
|
||||
}
|
||||
|
||||
@override
|
||||
State<DramaTipBubble> createState() => _DramaTipBubbleState();
|
||||
}
|
||||
|
||||
class _DramaTipBubbleState extends State<DramaTipBubble>
|
||||
with SingleTickerProviderStateMixin {
|
||||
//上下轻跳:0→1 映射成向上 0→4,来回循环
|
||||
late final _ani = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 600));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (DramaTipBubble.visible.value) _ani.repeat(reverse: true);
|
||||
DramaTipBubble.visible.addListener(_onVisibleChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
DramaTipBubble.visible.removeListener(_onVisibleChanged);
|
||||
_ani.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
//收起要停 ticker:repeat 的控制器不停会一直请求 vsync 帧
|
||||
void _onVisibleChanged() {
|
||||
DramaTipBubble.visible.value ? _ani.repeat(reverse: true) : _ani.stop();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!DramaTipBubble.visible.value) return const SizedBox.shrink();
|
||||
final logic = Get.find<MainPageLogic>();
|
||||
//底栏左右各 12 内边距,剩下的等分给各 tab;51.8 是尾巴尖在图里的横向位置
|
||||
final tabWidth = (Get.width - 24) / logic.tabs.length;
|
||||
final tailCenter = 12 + tabWidth * (DramaTipBubble.dramaTab + 0.5);
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(width: tailCenter - 51.8),
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.jumpToPage(DramaTipBubble.dramaTab),
|
||||
child: AnimatedBuilder(
|
||||
animation: _ani,
|
||||
builder: (_, child) => Transform.translate(
|
||||
offset: Offset(0, -4 * Curves.easeInOut.transform(_ani.value)),
|
||||
child: child,
|
||||
),
|
||||
child:
|
||||
Image.asset('drama_tip.webp'.homePath, width: 131, height: 35),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import 'package:hgdj/hj_model/home/plate_model.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/hj_utils/free_play_manager.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 '../../tools_base/toast.dart';
|
||||
import '../cartoon/cartoon_sub_module_logic.dart';
|
||||
import '../community/community/community_module_logic.dart';
|
||||
import '../community/community_main_page_page/community_main_logic.dart';
|
||||
import '../home/home_main_logic.dart';
|
||||
import '../home/provider/home_update_marker_provider.dart';
|
||||
import '../mine/mine_vip/vip_product_manager.dart';
|
||||
import '../short_video/short_main_video_logic.dart';
|
||||
import 'drama_tip_bubble.dart';
|
||||
import 'main_page.dart';
|
||||
import 'provider/msg_provider.dart';
|
||||
|
||||
class MainPageBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut(() => MainPageLogic());
|
||||
}
|
||||
}
|
||||
|
||||
/// 亚模块 id 在底栏里的落点
|
||||
class ModuleModel {
|
||||
final int tab; //底部 tab 下标
|
||||
final int sub; //顶部 tab 下标
|
||||
final int module; //顶部 tab 下的亚模块下标
|
||||
|
||||
ModuleModel({required this.tab, required this.sub, required this.module});
|
||||
}
|
||||
|
||||
//tab数据模型
|
||||
class MainTabModel {
|
||||
final String title; //title
|
||||
final String imageNor; //nor图片
|
||||
final String imageSel; //选中图片
|
||||
|
||||
MainTabModel({
|
||||
required this.title,
|
||||
required this.imageNor,
|
||||
required this.imageSel,
|
||||
});
|
||||
}
|
||||
|
||||
class MainPageLogic extends GetxController {
|
||||
final tabs = <MainTabModel>[
|
||||
MainTabModel(
|
||||
title: '主页',
|
||||
imageNor: 'home_0_0.png'.homePath,
|
||||
imageSel: 'home_0_1.png'.homePath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: '短剧',
|
||||
imageNor: 'home_1_0.png'.homePath,
|
||||
imageSel: 'home_1_1.png'.homePath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: Config.darkWebIconName?.isNotEmpty == true
|
||||
? Config.darkWebIconName!
|
||||
: '暗网',
|
||||
imageNor: 'home_2_0.png'.homePath,
|
||||
imageSel: 'home_2_1.png'.homePath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: 'AI',
|
||||
imageNor: 'home_3_0.png'.homePath,
|
||||
imageSel: 'home_3_1.png'.homePath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: '社区',
|
||||
imageNor: 'home_4_0.png'.homePath,
|
||||
imageSel: 'home_4_1.png'.homePath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: '我的',
|
||||
imageNor: 'home_5_0.png'.homePath,
|
||||
imageSel: 'home_5_1.png'.homePath,
|
||||
),
|
||||
];
|
||||
|
||||
/// 当前底部 tab 下标:内容页(LazyIndexedStack)和底栏选中态的唯一真相源。
|
||||
/// 启动固定落在短剧
|
||||
final curPageIndex = DramaTipBubble.dramaTab.obs;
|
||||
|
||||
final shortIndex = (-1).obs; // -1 = 用短剧频道的默认页(AI短剧)
|
||||
final darkInitTabId = Rx<String?>(null); // 暗网默认亚模块 id
|
||||
final communityIndex = 0.obs; // 社区模块默认模块
|
||||
final communitySubIndex = 0.obs; // 社区模块默认亚模块
|
||||
|
||||
/// 亚模块id -<主>tab - <模块 - 亚模块>
|
||||
final moduleMap = <String, ModuleModel>{};
|
||||
|
||||
late StreamSubscription _yinseSub;
|
||||
late StreamSubscription _loginSub;
|
||||
DateTime? _lastBackAt; //上次返回键点击时间
|
||||
|
||||
//暗网 tab 关掉后,排在它后面的 tab 整体前移一位
|
||||
int get aiTab => Config.darkWebEnable ? 3 : 2;
|
||||
|
||||
int get communityTab => Config.darkWebEnable ? 4 : 3;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (!Config.darkWebEnable) {
|
||||
tabs.removeAt(2);
|
||||
}
|
||||
MineMsgProvider().loadData();
|
||||
HomeUpdateMarkerProvider().loadData();
|
||||
|
||||
//启动就落在短剧:引导气泡不该再亮(这里早于 DramaTipBubble.start 的读盘)
|
||||
if (curPageIndex.value == DramaTipBubble.dramaTab) DramaTipBubble.hide();
|
||||
|
||||
_yinseSub = eventBus.on<YinseinnerModel>(_onYinse);
|
||||
_loginSub = eventBus.on<ReLoginEvent>(_onReLogin);
|
||||
|
||||
/// 组装所有
|
||||
Future(() {
|
||||
final app = Config.plateModule;
|
||||
if (app == null) return;
|
||||
void put(List<ModuleData> modules, int tab, int sub) {
|
||||
modules.asMap().forEach((i, e) =>
|
||||
moduleMap[e.id ?? ''] = ModuleModel(tab: tab, sub: sub, module: i));
|
||||
}
|
||||
|
||||
put(app.homePage, 0, 0);
|
||||
// 暗网 tab 关掉时不登记其亚模块:没有对应 tab,再按 tab=2 跳会落到 AI 页
|
||||
if (Config.darkWebEnable) put(app.deepWeb, 2, 0);
|
||||
put(app.community, communityTab, 0);
|
||||
put(app.picsUi, communityTab, 1);
|
||||
put(app.gameUi, communityTab, 2);
|
||||
put(app.novel, communityTab, 3);
|
||||
});
|
||||
}
|
||||
|
||||
void _onReLogin(ReLoginEvent event) {
|
||||
_backToRoot();
|
||||
jumpToPage(0);
|
||||
globalStore.refreshWallet();
|
||||
MineMsgProvider()
|
||||
.loadData(); //内含 refreshPayPopup:重拉支付分层配置,即时刷新浮窗/我的横幅/播放器横幅
|
||||
unawaited(FreePlayManager().refresh()); //切号:免费观看次数是账户维度的,不重拉会一直用着上个账号的次数
|
||||
HomeUpdateMarkerProvider().loadData();
|
||||
presaleProvider.refreshStatus(); //刷新预售权益
|
||||
vipProductManager.reloadForUser(); //切号:按新用户权限重拉会员卡(预售等卡随权限/购买状态返回不同)
|
||||
_homeLogic()?.tabCtr?.index = 0;
|
||||
}
|
||||
|
||||
void _onYinse(YinseinnerModel model) {
|
||||
switch (model.routeName) {
|
||||
case 'navigation_bar': //导航栏 亚模块菜单弹出
|
||||
_openNavBar();
|
||||
case 'module':
|
||||
case 'community_module': //媒体库亚模块分类
|
||||
_backToRoot();
|
||||
_matchModule(model.id);
|
||||
case 'live_streaming': //直播
|
||||
_backToRoot();
|
||||
_skipToLive();
|
||||
case 'ai':
|
||||
_backToRoot();
|
||||
_skipToAi();
|
||||
}
|
||||
}
|
||||
|
||||
//首页/暗网共用 HomeMainLogic,靠 tag 区分;页面还没建时返回 null
|
||||
HomeMainLogic? _homeLogic([String tag = 'HomeMainLogic']) =>
|
||||
Get.isRegistered<HomeMainLogic>(tag: tag)
|
||||
? Get.find<HomeMainLogic>(tag: tag)
|
||||
: null;
|
||||
|
||||
void _openNavBar() {
|
||||
_backToRoot();
|
||||
jumpToPage(0);
|
||||
_homeLogic()?.openEndDrawer();
|
||||
}
|
||||
|
||||
//跳转直播
|
||||
void _skipToLive() {
|
||||
jumpToPage(1);
|
||||
if (Get.isRegistered<ShortVideoMainLogic>()) {
|
||||
Get.find<ShortVideoMainLogic>().jumpToLive();
|
||||
} else {
|
||||
shortIndex.value = 0; //直播固定在第一个
|
||||
}
|
||||
}
|
||||
|
||||
//跳转ai
|
||||
void _skipToAi() => jumpToPage(aiTab);
|
||||
|
||||
void _matchModule(String? id) {
|
||||
if (id == null) return;
|
||||
final module = moduleMap[id];
|
||||
if (module == null) return;
|
||||
//首页和暗网 tab 可自动排序, 不用 HomeModuleProvider 处理
|
||||
if (module.tab == 0) {
|
||||
_homeLogic()?.gotoTabId(id);
|
||||
jumpToPage(0);
|
||||
} else if (module.tab == 2) {
|
||||
//tab==2 只在暗网开启时才登记进 moduleMap(见 onInit),所以这里下标可直接写 2
|
||||
final darkLogic = _homeLogic('HomeMainLogic_dark');
|
||||
if (darkLogic != null) {
|
||||
darkLogic.gotoTabId(id);
|
||||
} else {
|
||||
darkInitTabId.value = id;
|
||||
}
|
||||
jumpToPage(2);
|
||||
} else if (module.tab == communityTab) {
|
||||
_skipToCommunity(module);
|
||||
jumpToPage(communityTab);
|
||||
}
|
||||
}
|
||||
|
||||
//社区对应 logic 已建就直接切,没建就记下来等页面自己读
|
||||
void _skipToCommunity(ModuleModel module) {
|
||||
if (Get.isRegistered<CommunityMainLogic>()) {
|
||||
Get.find<CommunityMainLogic>().jumpSubModule(module.sub);
|
||||
if (module.sub == 0) {
|
||||
if (Get.isRegistered<CommunityMainModuleLogic>()) {
|
||||
Get.find<CommunityMainModuleLogic>().skipToTabIndex(module.module);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
final tag = [
|
||||
"",
|
||||
MediaStyle.Pic.name,
|
||||
MediaStyle.Game.name,
|
||||
MediaStyle.Novel.name
|
||||
][module.sub];
|
||||
if (Get.isRegistered<CartoonSubModuleLogic>(tag: tag)) {
|
||||
Get.find<CartoonSubModuleLogic>(tag: tag)
|
||||
.skipToTabIndex(module.module);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
communityIndex.value = module.sub;
|
||||
communitySubIndex.value = module.module;
|
||||
}
|
||||
|
||||
//返回首页
|
||||
void _backToRoot() =>
|
||||
Get.until((route) => route.settings.name == MainPage.routeName);
|
||||
|
||||
/// 切 tab:内容页和底栏选中态都监听 curPageIndex,改它一处即可
|
||||
void jumpToPage(int index) {
|
||||
curPageIndex.value = index;
|
||||
if (index == DramaTipBubble.dramaTab) DramaTipBubble.hide(); //进了短剧就别再指着它
|
||||
}
|
||||
|
||||
/// 双击返回退出:2 秒内连按两次返回键才真正退出
|
||||
bool confirmExit() {
|
||||
final now = DateTime.now();
|
||||
// 物理键,两次间隔大于 2 秒,退出请求无效
|
||||
if (_lastBackAt == null ||
|
||||
now.difference(_lastBackAt!) > const Duration(seconds: 2)) {
|
||||
_lastBackAt = now;
|
||||
showToast('再次返回即可退出');
|
||||
return false;
|
||||
}
|
||||
// 退出请求有效
|
||||
_lastBackAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_yinseSub.cancel();
|
||||
_loginSub.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/short_video/short_main_video_page.dart';
|
||||
import 'package:hgdj/tools_base/widget/lazy_indexed_stack.dart';
|
||||
|
||||
import '../../alert/vip_guide/guide_push_banner.dart';
|
||||
import '../../config/config.dart';
|
||||
import '../../main.dart';
|
||||
import '../ai/ai_home_page.dart';
|
||||
import '../community/community_main_page_page/community_main_page.dart';
|
||||
import '../home/dark_home_page.dart';
|
||||
import '../home/home_main_page.dart';
|
||||
import '../home/widget/home_txt_marquee.dart';
|
||||
import '../mine/home_mine_page.dart';
|
||||
import 'drama_tip_bubble.dart';
|
||||
import 'main_logic.dart';
|
||||
import 'main_tabbar.dart';
|
||||
|
||||
class MainPage extends StatefulWidget {
|
||||
static const routeName = '/MainPage';
|
||||
|
||||
const MainPage({super.key});
|
||||
|
||||
@override
|
||||
State<MainPage> createState() => _MainPageState();
|
||||
}
|
||||
|
||||
class _MainPageState extends State<MainPage> with RouteAware {
|
||||
// 懒加载:MainPage 可能在 binding 未跑时就被构造,
|
||||
// 用 getter 让 logic 只在 build 访问时才 Get.find,避免构造期 not found 崩溃
|
||||
MainPageLogic get logic => Get.find<MainPageLogic>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
GuidePushBanner.start(); //进 app 满3分钟后开始推 VIP「特享内容」横幅
|
||||
DramaTipBubble.start(); //「AI爽剧来袭」引导气泡,装机维度只亮一次
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route != null) routeObserver.subscribe(this, route);
|
||||
}
|
||||
|
||||
/// 二级页面/弹窗关闭回到本页:满3分钟后每次都拉一次「特享内容」,有数据就弹横幅
|
||||
@override
|
||||
void didPopNext() {
|
||||
super.didPopNext();
|
||||
GuidePushBanner.onBackToMainPage();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeObserver.unsubscribe(this);
|
||||
GuidePushBanner.stop(); //退登/重进流程销毁本页时,别留着定时任务空跑
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) return;
|
||||
if (logic.confirmExit()) SystemNavigator.pop();
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_buildPages(),
|
||||
//引导气泡贴着底栏上沿,尾巴指短剧 tab
|
||||
const Positioned(
|
||||
left: 0, right: 0, bottom: 0, child: DramaTipBubble()),
|
||||
],
|
||||
),
|
||||
),
|
||||
const MainPageTabbar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPages() {
|
||||
return Obx(() {
|
||||
final cur = logic.curPageIndex.value;
|
||||
return LazyIndexedStack(
|
||||
index: cur,
|
||||
children: [
|
||||
// 主页
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const HomeMainPage(),
|
||||
_bottomMarquee(),
|
||||
],
|
||||
),
|
||||
// 短视频
|
||||
Obx(() => ShortVideoMainPage(index: logic.shortIndex.value)),
|
||||
// 暗网
|
||||
if (Config.darkWebEnable)
|
||||
Obx(() => DarkHomePage(defaultId: logic.darkInitTabId.value)),
|
||||
// AI
|
||||
const AiHomePage(),
|
||||
// 社区
|
||||
Stack(
|
||||
children: [
|
||||
Obx(
|
||||
() => CommunityMainPage(
|
||||
index: logic.communityIndex.value,
|
||||
subTabIndex: logic.communitySubIndex.value,
|
||||
),
|
||||
),
|
||||
_bottomMarquee(),
|
||||
],
|
||||
),
|
||||
// 我的(IndexedStack 天然保活,不需要 keepAlive)
|
||||
const HomeMinePage(),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 首页/社区底部的文字跑马灯
|
||||
Widget _bottomMarquee() {
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: HomeTxtMarQuee(typeValue: 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_page/main_page/provider/bottom_bar_provider.dart';
|
||||
import 'package:hgdj/hj_page/pre_sale/pre_sale_provider.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../config/config.dart';
|
||||
import '../../../tools_base/event_bus/event_bus_util.dart';
|
||||
import '../../../tools_base/event_bus/events.dart';
|
||||
import '../home/provider/home_update_marker_provider.dart';
|
||||
import '../mine/home_mine_logic.dart';
|
||||
import 'main_logic.dart';
|
||||
|
||||
/// 底部导航栏:按 logic.tabs 渲染各 tab,选中态随 logic.curPageIndex 刷新
|
||||
class MainPageTabbar extends StatelessWidget {
|
||||
const MainPageTabbar({super.key});
|
||||
|
||||
MainPageLogic get logic => Get.find<MainPageLogic>();
|
||||
|
||||
void _selectIndex(int index) {
|
||||
final switchingToHome = index == 0 && logic.curPageIndex.value != 0;
|
||||
logic.jumpToPage(index);
|
||||
eventBus.emit(TabIndexChanged(index: index));
|
||||
//首页
|
||||
if (index == 0) {
|
||||
PreSaleProvider().refreshConfig(); //刷新预售权益
|
||||
// 从其它底栏切回主页时重拉「最新」等更新标记,避免长时间停留 App 内感知不到红点
|
||||
if (switchingToHome) {
|
||||
HomeUpdateMarkerProvider().loadData();
|
||||
}
|
||||
}
|
||||
// 短视频播/停不用在这里管:切走后 IndexedStack 不再 paint 该页,其 RepaintBoundary 图层
|
||||
// 从图层树摘除,VisibilityDetector 收到 detach 回调报可见度 0,播放器自行暂停。
|
||||
// 动短视频页的 RepaintBoundary 前,务必回归「切到别的 tab 还有没有声音」
|
||||
//我的:切到该 tab 时整体刷新(用户信息/钱包/消息/预售/分层配置)
|
||||
if (index == logic.tabs.length - 1 && Get.isRegistered<MineMainLogic>()) {
|
||||
Get.find<MineMainLogic>().onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//选中态与内容页同源 curPageIndex:底栏点击、deeplink 外部跳转都只改它一处
|
||||
return Obx(() {
|
||||
final cur = logic.curPageIndex.value;
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(12, 0, 12, screen.paddingBottom),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.black.withValues(alpha: 0.02))),
|
||||
),
|
||||
child: Row(
|
||||
children: List.generate(logic.tabs.length, (index) {
|
||||
if (index == 0)
|
||||
return _scrollTopItem<HomeBottomProvider>(index, cur);
|
||||
if (Config.darkWebEnable && index == 2)
|
||||
return _scrollTopItem<DarkwebBottomProvider>(index, cur);
|
||||
if (index == logic.communityTab)
|
||||
return _scrollTopItem<CommunityBottomProvider>(index, cur);
|
||||
return _normalItem(index, cur);
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 选中后再点会变「回到顶部」的 tab(主页/暗网/社区)
|
||||
Widget _scrollTopItem<T extends BaseBottomProvider>(int index, int cur) {
|
||||
final isSelected = index == cur;
|
||||
return Consumer<T>(builder: (_, provider, __) {
|
||||
final isTop = provider.showTop && isSelected;
|
||||
final imagePath =
|
||||
isTop ? 'jump_to_top.png'.homePath : _iconPath(index, isSelected);
|
||||
//暗网配置Icon
|
||||
final isDarkConfigImg = index == 2 &&
|
||||
isSelected &&
|
||||
Config.darkWebEnable &&
|
||||
Config.darkWebIcon?.isNotEmpty == true;
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => isTop ? provider.scrollToTop() : _selectIndex(index),
|
||||
child: _tabContent(
|
||||
isSelected: isSelected,
|
||||
text: isTop ? '顶部' : logic.tabs[index].title,
|
||||
icon: isDarkConfigImg
|
||||
? NetworkImageLoader(
|
||||
key: const ValueKey('darkIcon'),
|
||||
imageUrl: Config.darkWebIcon)
|
||||
: Image.asset(imagePath, key: ValueKey(imagePath), width: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// 普通 tab
|
||||
Widget _normalItem(int index, int cur) {
|
||||
final isSelected = index == cur;
|
||||
final imagePath = _iconPath(index, isSelected);
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => _selectIndex(index),
|
||||
child: _tabContent(
|
||||
isSelected: isSelected,
|
||||
text: logic.tabs[index].title,
|
||||
icon: Image.asset(imagePath, key: ValueKey(imagePath), width: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _iconPath(int index, bool isSelected) =>
|
||||
isSelected ? logic.tabs[index].imageSel : logic.tabs[index].imageNor;
|
||||
|
||||
/// 图标 + 文案:图标切换(key 变即图片路径变)交叉淡入,文字颜色平滑过渡
|
||||
Widget _tabContent(
|
||||
{required Widget icon, required bool isSelected, required String text}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180), child: icon),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? const Color(0xffF68804)
|
||||
: const Color(0xff656565),
|
||||
fontSize: 10,
|
||||
),
|
||||
child: Text(text),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
|
||||
class BaseBottomProvider extends ChangeNotifier {
|
||||
bool showTop = false; //选中的 tab 是否变成「回到顶部」
|
||||
|
||||
final _edge = 500.0; //滚动临界值
|
||||
final _ctrCaches = <int, ScrollController>{};
|
||||
ScrollController? _curCtr;
|
||||
|
||||
/// 绑定第 [index] 个 tab 的滚动控制器,[controller] 不传则复用缓存里的
|
||||
void setScrollController(int index,
|
||||
{ScrollController? controller, bool forceToTop = false}) {
|
||||
controller ??= _ctrCaches[index];
|
||||
if (controller == null) {
|
||||
_curCtr?.removeListener(_onScroll); //没有可绑的目标,旧监听也一并摘掉
|
||||
return;
|
||||
}
|
||||
_ctrCaches[index] = controller;
|
||||
showTop = !forceToTop && controller.hasClients && controller.offset > _edge;
|
||||
_bind(controller);
|
||||
}
|
||||
|
||||
//换绑监听目标:摘旧挂新,notify 排到下一帧,避免在 build 期间通知
|
||||
void _bind(ScrollController ctr) {
|
||||
_curCtr?.removeListener(_onScroll);
|
||||
_curCtr = ctr;
|
||||
ctr.addListener(_onScroll);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => notifyListeners());
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
final overEdge = (_curCtr?.offset ?? 0) > _edge;
|
||||
if (overEdge != showTop) {
|
||||
showTop = overEdge;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToTop() {
|
||||
if (_curCtr?.hasClients == true) {
|
||||
_curCtr?.jumpTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HomeBottomProvider extends BaseBottomProvider {
|
||||
static final HomeBottomProvider _instance = HomeBottomProvider._();
|
||||
|
||||
factory HomeBottomProvider() => _instance;
|
||||
|
||||
HomeBottomProvider._();
|
||||
}
|
||||
|
||||
class DarkwebBottomProvider extends BaseBottomProvider {
|
||||
static final DarkwebBottomProvider _instance = DarkwebBottomProvider._();
|
||||
|
||||
factory DarkwebBottomProvider() => _instance;
|
||||
|
||||
DarkwebBottomProvider._();
|
||||
}
|
||||
|
||||
class CommunityBottomProvider extends BaseBottomProvider {
|
||||
static final CommunityBottomProvider _instance = CommunityBottomProvider._();
|
||||
|
||||
factory CommunityBottomProvider() => _instance;
|
||||
|
||||
CommunityBottomProvider._();
|
||||
|
||||
//社区分区(社区/图集/黄游/小说) → 各 tab 的滚动控制器
|
||||
final _caches = <MediaStyle, Map<int, ScrollController>>{};
|
||||
final _typeIndexes = <MediaStyle, int>{};
|
||||
|
||||
MediaStyle _type = MediaStyle.Community;
|
||||
|
||||
/// 取某分区某 tab 的滚动控制器(没有就建并缓存),第一个 tab 顺带绑定监听
|
||||
ScrollController scrollCtr(MediaStyle type, int tabIndex) {
|
||||
final ctr = (_caches[type] ??= {})[tabIndex] ??= ScrollController();
|
||||
if (tabIndex == 0) _bind(ctr);
|
||||
return ctr;
|
||||
}
|
||||
|
||||
set communityType(MediaStyle t) {
|
||||
_type = t;
|
||||
_updateState();
|
||||
}
|
||||
|
||||
set subIndex(int index) {
|
||||
_typeIndexes[_type] = index;
|
||||
_updateState();
|
||||
}
|
||||
|
||||
void _updateState() {
|
||||
final ctr = _caches[_type]?[_typeIndexes[_type] ?? 0];
|
||||
if (ctr == null) return;
|
||||
showTop = ctr.hasClients && ctr.offset > _edge;
|
||||
_bind(ctr);
|
||||
}
|
||||
|
||||
@Deprecated('请使用 scrollCtr / subIndex')
|
||||
@override
|
||||
void setScrollController(int index,
|
||||
{ScrollController? controller, bool forceToTop = false}) {}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_model/splash/domain_source_model.dart';
|
||||
import 'package:hgdj/hj_page/mine/mine_vip/vip_product_manager.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/global_store/store.dart';
|
||||
|
||||
import '../../../hj_utils/light_model.dart';
|
||||
|
||||
/// 我的未读消息,和分层优惠倒计时
|
||||
class MineMsgProvider with ChangeNotifier {
|
||||
static final MineMsgProvider _instance = MineMsgProvider._();
|
||||
MineMsgProvider._();
|
||||
factory MineMsgProvider() => _instance;
|
||||
|
||||
bool _hasNewMsg = false; //消息中心小红点
|
||||
|
||||
/// 优惠倒计时配置(全站唯一数据源),无配置/已过期时为 null
|
||||
PayTierConfig? get countdownConfig {
|
||||
final config = payTier?.config;
|
||||
return config?.hasDiscountCountdown == true ? config : null;
|
||||
}
|
||||
|
||||
void loadData() {
|
||||
getMessageTip();
|
||||
refreshPayPopup();
|
||||
}
|
||||
|
||||
Future<void> getMessageTip() async {
|
||||
final res = await MineService.checkMessageTip();
|
||||
_hasNewMsg = res?.newsTip ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ========== 支付状态分层(PayTier)==========
|
||||
/// 支付状态分层配置(分层/卡ID/各展示位图片)——全站唯一数据源,原 Config.paymentStatusPopup 迁来此统一管理
|
||||
PayTierModel? payTier;
|
||||
|
||||
static String get _payPopupShownKey =>
|
||||
"PayLayeredPopup_shown_${globalStore.meInfo?.uid}"; // 支付分层首弹是否已弹过
|
||||
final ValueNotifier<int> tick = ValueNotifier<int>(
|
||||
0); //分层倒计时秒级 tick,倒计时块用 ValueListenableBuilder 监听它,避免每秒重建整卡/图片
|
||||
Timer? _payTimer; //支付分层倒计时(基于 config.lastDiscountTime)
|
||||
int? _lastExpireRefreshAt; //上次"归零刷新"的时间戳(秒),用于 30s 防抖
|
||||
|
||||
/// 刷新用户分层弹窗配置(payTier),成功后写回并通知 UI 刷新
|
||||
Future<PayTierModel?> refreshPayPopup() async {
|
||||
final model = await CommonService.refreshPayPopup(
|
||||
keys: ['paymentPopup', 'paymentGuide']);
|
||||
// 容错:接口返回空数据时 model 为 null,保留本地状态不替换
|
||||
if (model != null) {
|
||||
// 分层状态前后不一致 → 用户权益/升级状态已变(VIP 支付是异步的,以分层变化为准),清会员卡缓存下次重开强制重拉
|
||||
final statusChanged = payTier?.status != model.status;
|
||||
payTier = model;
|
||||
if (statusChanged) {
|
||||
vipProductManager.invalidate();
|
||||
}
|
||||
startPayTimer(); //刷新后按新的截止时刻重启倒计时
|
||||
notifyListeners();
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/// 启动支付分层倒计时:仅当优惠尚未截止才开,每秒自增 [tick] 驱动倒计时块刷新,过期自动停。
|
||||
/// 注意:倒计时块用 ValueListenableBuilder(MineMsgProvider().tick) 监听(只重建倒计时);
|
||||
/// 整卡/横幅/浮窗的图片仍用 Consumer<MineMsgProvider>(配置刷新时才重建)。
|
||||
void startPayTimer() {
|
||||
_payTimer?.cancel();
|
||||
_payTimer = null;
|
||||
final config = payTier?.config;
|
||||
if (config == null || !config.hasDiscountCountdown) return;
|
||||
_payTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
// 过期:停表 + 收起倒计时,并拉一次最新分层(优惠结束后分层大概率已变)
|
||||
if (!(payTier?.config?.hasDiscountCountdown ?? false)) {
|
||||
_payTimer?.cancel();
|
||||
_payTimer = null;
|
||||
_refreshOnExpire();
|
||||
}
|
||||
tick.value++;
|
||||
});
|
||||
}
|
||||
|
||||
/// 支付分层首弹是否已弹过(不分新人/老人,全局只弹一次)
|
||||
static Future<bool> isPayPopupShown() async =>
|
||||
await lightKV.getBool(_payPopupShownKey) ?? false;
|
||||
|
||||
static Future<void> markPayPopupShown() async =>
|
||||
await lightKV.setBool(_payPopupShownKey, true);
|
||||
|
||||
/// 倒计时归零后刷新分层配置(带 30s 防抖,避免后端返回临界时间导致频繁刷新)
|
||||
void _refreshOnExpire() {
|
||||
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
if (_lastExpireRefreshAt != null && nowSec - _lastExpireRefreshAt! < 30)
|
||||
return;
|
||||
_lastExpireRefreshAt = nowSec;
|
||||
refreshPayPopup(); // 成功后内部会按新 lastDiscountTime 重启/不重启计时
|
||||
}
|
||||
|
||||
//红点view
|
||||
Widget buildTipsView() {
|
||||
return _hasNewMsg
|
||||
? Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xfff74f49),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
)
|
||||
: SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class NewMessageTip {
|
||||
bool? newsTip;
|
||||
|
||||
NewMessageTip({this.newsTip});
|
||||
|
||||
NewMessageTip.fromJson(dynamic json) {
|
||||
newsTip = json['newsTip'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user