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

custom_page.dart 15 KiB

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