基础组件库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

442 line
14 KiB

  1. import 'dart:convert';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:pull_to_refresh/pull_to_refresh.dart';
  5. import 'package:tab_indicator_styler/tab_indicator_styler.dart';
  6. import 'package:zhiying_base_widget/pages/custom_page/custom_item_page.dart';
  7. import 'package:zhiying_base_widget/pages/main_page/model/background_model.dart';
  8. import 'package:zhiying_base_widget/pages/main_page/notifier/main_page_bg_notifier.dart';
  9. import 'package:zhiying_base_widget/widgets/custom/search/custom_search_widget.dart';
  10. import 'package:zhiying_base_widget/widgets/empty/empty_widget.dart';
  11. import 'package:zhiying_base_widget/widgets/others/mine_header_bg_widget.dart';
  12. import 'package:zhiying_comm/zhiying_comm.dart';
  13. import 'package:flutter/material.dart';
  14. import 'package:provider/provider.dart';
  15. import 'package:flutter_bloc/flutter_bloc.dart';
  16. import 'bloc/background_bloc.dart';
  17. import 'bloc/custom_page_bloc.dart';
  18. import 'bloc/custom_page_state.dart';
  19. import 'bloc/custom_page_event.dart';
  20. import 'bloc/custom_page_repository.dart';
  21. import 'dart:ui';
  22. import 'package:fluttertoast/fluttertoast.dart';
  23. ///
  24. /// 通用模块页面
  25. ///
  26. class CustomPage extends StatefulWidget {
  27. final Map<String, dynamic> data;
  28. CustomPage(this.data, {Key key}) : super(key: key);
  29. @override
  30. _CustomPageState createState() => _CustomPageState();
  31. }
  32. class _CustomPageState extends State<CustomPage> {
  33. @override
  34. Widget build(BuildContext context) {
  35. Logger.log("数据: " + widget?.data.toString());
  36. return MultiProvider(
  37. providers: [
  38. ChangeNotifierProvider.value(value: MainPageBgNotifier()),
  39. ],
  40. child: BlocProvider<CustomPageBloc>(
  41. create: (_) => CustomPageBloc(CustomPageRepository(data: widget?.data))..add(CustomPageInitEvent()),
  42. child: _CommonPageContainer(widget?.data),
  43. // ),
  44. ),
  45. );
  46. }
  47. }
  48. class _CommonPageContainer extends StatefulWidget {
  49. final Map<String, dynamic> data;
  50. _CommonPageContainer(this.data);
  51. @override
  52. __CommonPageContainerState createState() => __CommonPageContainerState();
  53. }
  54. class __CommonPageContainerState extends State<_CommonPageContainer> with SingleTickerProviderStateMixin ,AutomaticKeepAliveClientMixin{
  55. TabController _tabController;
  56. // 是否有AppBar
  57. bool _isHasAppbar = false;
  58. // 是否有TabBar
  59. bool _isHasTabBar = false;
  60. double backgroundTopMargin = 0;
  61. BackgroundBloc backgroundBloc;
  62. /// 刷新
  63. void _onRefreshEvent() async {
  64. BlocProvider.of<CustomPageBloc>(context).add(CustomPageRefreshEvent());
  65. }
  66. @override
  67. void initState() {
  68. backgroundBloc=BackgroundBloc();
  69. super.initState();
  70. }
  71. @override
  72. void dispose() {
  73. _tabController?.dispose();
  74. backgroundBloc.streamController.close();
  75. super.dispose();
  76. }
  77. @override
  78. Widget build(BuildContext context) {
  79. SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
  80. return MediaQuery.removePadding(
  81. context: context,
  82. child: BlocConsumer<CustomPageBloc, CustomPageState>(
  83. listener: (context, state) {},
  84. buildWhen: (prev, current) {
  85. if (current is CustomPageErrorState) {
  86. return false;
  87. }
  88. if (current is CustomPageRefreshSuccessState) {
  89. // _refreshController.refreshCompleted(resetFooterState: true);
  90. return false;
  91. }
  92. if (current is CustomPageRefreshErrorState) {
  93. // _refreshController.refreshFailed();
  94. return false;
  95. }
  96. return true;
  97. },
  98. builder: (context, state) {
  99. /// 有数据
  100. if (state is CustomPageLoadedState) {
  101. if (EmptyUtil.isEmpty(state.model)) return _buildEmptyWidget();
  102. return _buildMainWidget(state.model, state.backgroundModel);
  103. }
  104. /// 初始化失败
  105. if (state is CustomPageInitErrorState) {
  106. return _buildEmptyWidget();
  107. }
  108. /// 骨架图
  109. return _buildSkeletonWidget();
  110. },
  111. ),
  112. );
  113. }
  114. /// 有数据
  115. Widget _buildMainWidget(List<Map<String, dynamic>> model, BackgroundModel backgroundModel) {
  116. return Scaffold(
  117. appBar: _buildAppbar(model?.first),
  118. backgroundColor: HexColor.fromHex(backgroundModel?.bgColor == null || backgroundModel?.bgColor == '' ? "#FFF9F9F9" : backgroundModel?.bgColor ?? "#FFF9F9F9"),
  119. // floatingActionButton: _buildFloatWidget(),
  120. floatingActionButtonLocation: _CustomFloatingActionButtonLocation(FloatingActionButtonLocation.endFloat, 0, -100),
  121. body: Stack(
  122. children: <Widget>[
  123. _buildBackground(backgroundModel),
  124. Column(children: _buildFirstWidget(model, backgroundModel)),
  125. ],
  126. ),
  127. );
  128. }
  129. /// 骨架图
  130. Widget _buildSkeletonWidget() {
  131. return Scaffold();
  132. }
  133. /// 空数据视图
  134. Widget _buildEmptyWidget() {
  135. return Scaffold(
  136. backgroundColor: HexColor.fromHex('#F9F9F9'),
  137. appBar: AppBar(
  138. brightness: Brightness.light,
  139. backgroundColor: Colors.white,
  140. leading: IconButton(
  141. icon: Icon(
  142. Icons.arrow_back_ios,
  143. size: 22,
  144. color: HexColor.fromHex('#333333'),
  145. ),
  146. onPressed: () => Navigator.maybePop(context),
  147. ),
  148. title: Text(
  149. '',
  150. style: TextStyle(color: HexColor.fromHex('#333333'), fontSize: 18, fontWeight: FontWeight.bold),
  151. ),
  152. centerTitle: true,
  153. elevation: 0,
  154. ),
  155. body: Column(
  156. crossAxisAlignment: CrossAxisAlignment.center,
  157. mainAxisAlignment: MainAxisAlignment.center,
  158. children: <Widget>[
  159. Align(
  160. alignment: Alignment.topCenter,
  161. child: EmptyWidget(
  162. tips: '网络似乎开小差了~',
  163. ),
  164. ),
  165. GestureDetector(
  166. onTap: () => _onRefreshEvent(),
  167. behavior: HitTestBehavior.opaque,
  168. child: Container(
  169. alignment: Alignment.center,
  170. decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), border: Border.all(color: HexColor.fromHex('#999999'), width: 0.5), color: Colors.white),
  171. width: 80,
  172. height: 20,
  173. child: Text(
  174. '刷新一下',
  175. style: TextStyle(fontSize: 12, color: HexColor.fromHex('#333333'), fontWeight: FontWeight.bold),
  176. ),
  177. ),
  178. )
  179. ],
  180. ));
  181. }
  182. /// 数据,生成第一层widget
  183. List<Widget> _buildFirstWidget(List<Map<String, dynamic>> model, BackgroundModel backgroundModel) {
  184. List<Widget> result = [];
  185. // 分类导航的key ⚠️ 这里先写成Test 后续要改
  186. const String CATEGORY_KEY = 'category';
  187. // 判断是否有分类导航
  188. // 判断最后一个是否属于分类导航,如果属于,则有分类导航,如果不是,则无分类导航
  189. bool haveCategory = !EmptyUtil.isEmpty(model?.last) && model.last.containsKey('mod_name') && model.last['mod_name'] == CATEGORY_KEY;
  190. int endIndexLength = model.length;
  191. // 如果没有分类导航,则取分类导航之上的所有mod
  192. if (!haveCategory) {
  193. for (int i = 0; i < model.length; i++) {
  194. Map<String, dynamic> item = model[i];
  195. if (item['mod_name'] == CATEGORY_KEY) {
  196. endIndexLength = (i + 1);
  197. break;
  198. }
  199. }
  200. }
  201. for (int i = 0; i < endIndexLength; i++) {
  202. WidgetModel item = WidgetModel.fromJson(Map<String, dynamic>.from(model[i]));
  203. // last model
  204. if (i == endIndexLength - 1) {
  205. result.addAll(_buildTabBar(model[i], i));
  206. break;
  207. }
  208. // appBar 无需在这里添加
  209. if (item.modName.contains('appbar')) {
  210. continue;
  211. }
  212. result.addAll(WidgetFactory.create(item.modName, isSliver: false, model: model[i]));
  213. }
  214. // 没有appbar并且没有tabbar,则给第一个元素加边距
  215. if (!_isHasAppbar) {
  216. result.insert(0, SizedBox(height: MediaQueryData.fromWindow(window).padding.top, child: Container(color: HexColor.fromHex(backgroundModel?.headerBg?.mainColor))));
  217. }
  218. return result;
  219. }
  220. /// appbar
  221. Widget _buildAppbar(final Map<String, dynamic> model) {
  222. if (EmptyUtil.isEmpty(model)) return null;
  223. String mobName = model['mod_name'];
  224. if (!mobName.contains('_appbar')) return null;
  225. Map<String, dynamic> data = Map<String, dynamic>();
  226. try {
  227. data = jsonDecode(model['data']);
  228. } catch (e, s) {
  229. Logger.warn(e, s);
  230. }
  231. String parentTitle = !EmptyUtil.isEmpty(widget?.data) ? widget?.data['title'] ?? '' : '';
  232. _isHasAppbar = true;
  233. return AppBar(
  234. backgroundColor: HexColor.fromHex(null != data ? data['app_bar_bg_color'] ?? '#FFFFFF' : '#FFFFFF'),
  235. brightness: Brightness.light,
  236. leading: IconButton(
  237. icon: Icon(
  238. Icons.arrow_back_ios,
  239. size: 22,
  240. color: HexColor.fromHex('#333333'),
  241. ),
  242. onPressed: () => Navigator.maybePop(context),
  243. ),
  244. title: Text(
  245. null != data && data.containsKey('app_bar_name') ? data['app_bar_name'] != '自定义页面' ? data['app_bar_name'] : parentTitle : parentTitle,
  246. style: TextStyle(
  247. color: HexColor.fromHex(null != data ? data['app_bar_name_color'] ?? '#333333' : '#333333'),
  248. fontSize: 16,
  249. fontWeight: FontWeight.bold,
  250. ),
  251. ),
  252. centerTitle: true,
  253. elevation: 0,
  254. );
  255. }
  256. /// tabBar
  257. List<Widget> _buildTabBar(final Map<String, dynamic> model, final int index) {
  258. Map<String, dynamic> data = Map<String, dynamic>();
  259. List<Map<String, dynamic>> listStyle = [];
  260. List<Widget> result = [];
  261. try {
  262. data = jsonDecode(model['data']);
  263. listStyle = List.from(data['list_style']);
  264. } catch (e, s) {
  265. Logger.warn(e, s);
  266. }
  267. // 1、导航栏没开启的情况 传null进去进行获取没开启导航栏的widget集合
  268. if (EmptyUtil.isEmpty(listStyle)) {
  269. result.add(Expanded(
  270. child: CustomItemPage(
  271. null,
  272. 0,
  273. model['mod_id']?.toString() ?? null,
  274. model['mod_pid']?.toString() ?? null,
  275. (!_isHasAppbar && index == 0),
  276. scroller: _listenScroller,
  277. ),
  278. ));
  279. return result;
  280. }
  281. // 2、导航栏开启的情况
  282. if (listStyle.length > 0) {
  283. // tabContorller 初始化
  284. if (null == _tabController || _tabController.length != listStyle.length) {
  285. _tabController = new TabController(length: listStyle.length, vsync: this);
  286. }
  287. result.add(Container(
  288. height: 40,
  289. width: double.infinity,
  290. color: HexColor.fromHex(data['bg_color']),
  291. child: TabBar(
  292. controller: _tabController,
  293. isScrollable: /*listStyle.length <= 5 ? false : */ true,
  294. labelColor: HexColor.fromHex(data['choose_text_color'] ?? '#FF4242'),
  295. labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
  296. unselectedLabelColor: HexColor.fromHex(data['text_color'] ?? '#999999'),
  297. unselectedLabelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
  298. indicatorSize: TabBarIndicatorSize.label,
  299. indicator: MaterialIndicator(
  300. color: HexColor.fromHex(data['choose_color'] ?? '#FF4242'),
  301. bottomLeftRadius: 1.25,
  302. topLeftRadius: 1.25,
  303. topRightRadius: 1.25,
  304. bottomRightRadius: 1.25,
  305. height: 2.5,
  306. horizontalPadding: 5,
  307. ),
  308. tabs: listStyle.map((e) => Text(e['name'])).toList(),
  309. ),
  310. ));
  311. _isHasTabBar = true;
  312. // 最后添加TabBarView
  313. result.add(Expanded(
  314. child: TabBarView(
  315. controller: _tabController,
  316. children: _buildTabBarViewChildren(listStyle, model['mod_id']?.toString(), model['mod_pid']?.toString(), index),
  317. ),
  318. ));
  319. }
  320. return result;
  321. }
  322. /// 返回TabBarView的视图
  323. List<Widget> _buildTabBarViewChildren(final List<Map<String, dynamic>> listStyle, final String modId, final String modPid, final int index) {
  324. List<Widget> result = [];
  325. for (int i = 0; i < listStyle.length; i++) {
  326. result.add(CustomItemPage(
  327. listStyle[i],
  328. i,
  329. modId,
  330. modPid,
  331. (!_isHasAppbar && !_isHasTabBar && index == 0),
  332. scroller: _listenScroller,
  333. ));
  334. }
  335. return result;
  336. }
  337. _buildBackground(BackgroundModel backgroundModel) {
  338. if (backgroundModel != null) {
  339. var headerBg = backgroundModel.headerBg;
  340. return StreamBuilder(
  341. stream: backgroundBloc.outData,
  342. builder: (context,asncy){
  343. return Container(
  344. constraints: BoxConstraints(minHeight: 0),
  345. height: (double.tryParse(headerBg?.height)??0)+backgroundTopMargin ?? 0,
  346. width: double.infinity,
  347. decoration: BoxDecoration(
  348. gradient: LinearGradient(
  349. begin: Alignment.topCenter,
  350. end: Alignment.bottomCenter,
  351. colors: [HexColor.fromHex(headerBg?.mainColor ?? ""), HexColor.fromHex(headerBg?.assistColor ?? ""), HexColor.fromHex(headerBg?.minorColor ?? "")])),
  352. );
  353. },
  354. );
  355. } else {
  356. return Container();
  357. }
  358. }
  359. // /// 悬浮按钮
  360. // Widget _buildFloatWidget() {
  361. // return Visibility(
  362. // visible: true,
  363. // child: GestureDetector(
  364. // onTap: () => _scrollTop(),
  365. // behavior: HitTestBehavior.opaque,
  366. // child: Container(
  367. // height: 30,
  368. // width: 30,
  369. // child: Icon(Icons.arrow_upward),
  370. // ),
  371. // ),
  372. // );
  373. // }
  374. ///监听页面滚动
  375. _listenScroller(double offset) {
  376. if (offset >= 0) {
  377. backgroundTopMargin = -offset;
  378. if (backgroundTopMargin > -500) {
  379. backgroundBloc.streamController.add("");
  380. }
  381. }
  382. }
  383. @override
  384. // TODO: implement wantKeepAlive
  385. bool get wantKeepAlive =>true;
  386. }
  387. /// 回到顶部的icon
  388. class _CustomFloatingActionButtonLocation extends FloatingActionButtonLocation {
  389. FloatingActionButtonLocation location;
  390. double offsetX; // X方向的偏移量
  391. double offsetY; // Y方向的偏移量
  392. _CustomFloatingActionButtonLocation(this.location, this.offsetX, this.offsetY);
  393. @override
  394. Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
  395. Offset offset = location.getOffset(scaffoldGeometry);
  396. return Offset(offset.dx + offsetX, offset.dy + offsetY);
  397. }
  398. }