基础组件库
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.
 
 
 
 
 

436 lines
14 KiB

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