product_details.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_html/flutter_html.dart';
  4. import 'package:flutter_swiper/flutter_swiper.dart';
  5. import 'package:cached_network_image/cached_network_image.dart';
  6. import 'package:twong/api/index.dart';
  7. import 'package:twong/utils/index.dart';
  8. import 'package:twong/config/style.dart';
  9. import 'package:twong/router/index.dart';
  10. import 'package:twong/models/index.dart';
  11. import 'package:twong/widgets/future_widget.dart';
  12. import 'package:twong/pages/product/product_attr.dart';
  13. import 'package:twong/widgets/share_bar.dart';
  14. class ProductDetailsPage extends StatefulWidget {
  15. final dynamic id;
  16. ProductDetailsPage (this.id, {Key key}): super(key: key);
  17. @override
  18. State<StatefulWidget> createState() {
  19. return _ProductDetailsPageState();
  20. }
  21. }
  22. class _ProductDetailsPageState extends State<ProductDetailsPage> {
  23. Details _details;
  24. IconData _likeIcon = Icons.favorite_border;
  25. @override
  26. void initState() {
  27. super.initState();
  28. }
  29. void _onAddCart () {
  30. showModalBottomSheet(
  31. context: context,
  32. shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(
  33. topLeft: Radius.circular(10.px), topRight: Radius.circular(10.px)
  34. )),
  35. builder: (BuildContext context) {
  36. return ProductAttrPage(_details, isCart: true);
  37. }
  38. );
  39. }
  40. void _onBuy () {
  41. showModalBottomSheet(
  42. context: context,
  43. shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(
  44. topLeft: Radius.circular(10.px), topRight: Radius.circular(10.px)
  45. )),
  46. builder: (BuildContext context) {
  47. return ProductAttrPage(_details);
  48. });
  49. }
  50. @override
  51. Widget build(BuildContext context) {
  52. return Scaffold(
  53. backgroundColor: Colors.white,
  54. body: FutureWidget(Network.inst.getDetail, _buildBody, data: widget.id),
  55. bottomNavigationBar: SafeArea(
  56. child: Container(
  57. height: 60.px,
  58. color: Colors.white,
  59. child: Flex(
  60. direction: Axis.horizontal,
  61. children: <Widget>[
  62. Row(
  63. children: <Widget>[
  64. GestureDetector(
  65. onTap: () {
  66. Navigator.pushNamed(
  67. context, RouteNames.message, arguments: false);
  68. },
  69. child: Container(
  70. width: 60.px,
  71. padding: EdgeInsets.only(
  72. left: 16.px, right: 12.px, top: 12.px),
  73. child: Column(
  74. children: <Widget>[
  75. Icon(Icons.message),
  76. Text('客服', style: TextStyle(fontSize: 10.px),)
  77. ],
  78. ),
  79. )
  80. ),
  81. GestureDetector(
  82. onTap: () {
  83. Navigator.pushNamed(
  84. context, RouteNames.cart, arguments: true);
  85. },
  86. child: Container(
  87. padding: EdgeInsets.only(
  88. left: 12.px, right: 12.px, top: 12.px),
  89. child: Column(
  90. children: <Widget>[
  91. Icon(Icons.shopping_cart),
  92. Text('购物车', style: TextStyle(fontSize: 10.px),)
  93. ],
  94. ),
  95. )
  96. ),
  97. ],
  98. ),
  99. Expanded(child: Row(
  100. crossAxisAlignment: CrossAxisAlignment.center,
  101. children: <Widget>[
  102. Expanded(
  103. child: Container(
  104. padding: EdgeInsets.all(6.px),
  105. child: FlatButton(
  106. onPressed: _onAddCart,
  107. color: Colors.black,
  108. shape: StadiumBorder(),
  109. child: Text('加入购物车',
  110. style: TextStyle(color: Colors.white)),
  111. )
  112. ),
  113. ),
  114. Expanded(
  115. child: Container(
  116. padding: EdgeInsets.all(6.px),
  117. child: FlatButton(
  118. onPressed: _onBuy,
  119. color: Colors.red,
  120. shape: StadiumBorder(),
  121. child: Text('立即购买',
  122. style: TextStyle(color: Colors.white)),
  123. )
  124. ),
  125. ),
  126. ],
  127. )),
  128. ],
  129. ),
  130. ),
  131. )
  132. );
  133. }
  134. Widget _buildBody(dynamic data) {
  135. _details = data;
  136. return CustomScrollView(
  137. physics: ClampingScrollPhysics(),
  138. slivers: <Widget>[
  139. SliverPersistentHeader(
  140. pinned: true,
  141. delegate: SliverCustomHeaderDelegate(
  142. paddingTop: MediaQuery.of(context).padding.top,
  143. data: _details, likeClick: _likeClick, likeIcon: _likeIcon),
  144. ),
  145. SliverToBoxAdapter(
  146. child: ProductContent(_details),
  147. )
  148. ],
  149. );
  150. }
  151. void _likeClick() async {
  152. if(_likeIcon == Icons.favorite_border) {
  153. setState(() {
  154. _likeIcon = Icons.favorite;
  155. });
  156. } else {
  157. setState(() {
  158. _likeIcon = Icons.favorite_border;
  159. });
  160. }
  161. }
  162. }
  163. class SliverCustomHeaderDelegate extends SliverPersistentHeaderDelegate {
  164. final double collapsedHeight = 40.px;
  165. final double expandedHeight = 400.px;
  166. final double paddingTop;
  167. final Details data;
  168. final IconData likeIcon;
  169. final Function likeClick;
  170. final Color topColor = DColors.Main;
  171. int tabIdx = 0;
  172. String statusBarMode = 'dark';
  173. final Color tColor = Color.fromARGB(168, DColors.Main.red, DColors.Main.green, DColors.Main.blue);
  174. SliverCustomHeaderDelegate({this.paddingTop, this.data, this.likeClick, this.likeIcon});
  175. @override
  176. double get maxExtent => this.expandedHeight;
  177. @override
  178. double get minExtent => this.collapsedHeight + this.paddingTop;
  179. @override
  180. bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
  181. void updateStatusBarBrightness(shrinkOffset) {
  182. if (shrinkOffset > 50.px && this.statusBarMode == 'light') {
  183. this.statusBarMode = 'dark';
  184. SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  185. statusBarBrightness: Brightness.dark,
  186. statusBarIconBrightness: Brightness.dark,
  187. ));
  188. } else if (shrinkOffset <= 50.px && this.statusBarMode == 'dark') {
  189. this.statusBarMode = 'light';
  190. SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  191. statusBarBrightness: Brightness.light,
  192. statusBarIconBrightness: Brightness.light,
  193. ));
  194. }
  195. }
  196. Color makeStickyHeaderBgColor(shrinkOffset) {
  197. final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
  198. .clamp(0, 255)
  199. .toInt();
  200. return Color.fromARGB(alpha, tColor.red, tColor.green, tColor.blue);
  201. }
  202. Color makeStickyHeaderTextColor(shrinkOffset, isIcon) {
  203. if (shrinkOffset <= 50.px) {
  204. return isIcon ? DColors.Main : Colors.transparent;
  205. } else {
  206. final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
  207. .clamp(0, 255).toInt();
  208. return Color.fromARGB(alpha, 255, 255, 255);
  209. }
  210. }
  211. Widget _buildSlider(BuildContext context, int index) {
  212. return CachedNetworkImage(
  213. fit: BoxFit.cover,
  214. imageUrl: data.storeInfo.slider_image[index],
  215. placeholder: (BuildContext context, String str) {
  216. return Center(child: Utils.loadingWidget);
  217. },
  218. );
  219. }
  220. @override
  221. Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
  222. this.updateStatusBarBrightness(shrinkOffset);
  223. var images = data.storeInfo.slider_image;
  224. return Container(
  225. height: this.maxExtent,
  226. width: MediaQuery
  227. .of(context)
  228. .size
  229. .width,
  230. child: Stack(
  231. fit: StackFit.expand,
  232. children: <Widget>[
  233. Container(
  234. height: 400.px,
  235. child: Swiper(
  236. autoplay: true,
  237. itemCount: images.length,
  238. itemBuilder: _buildSlider,
  239. pagination: SwiperPagination(),
  240. autoplayDisableOnInteraction: true,
  241. onTap: (index) {
  242. Utils.showPhoto(index: index, images: images);
  243. }
  244. )
  245. ),
  246. Positioned(left: 0, right: 0, top: 0,
  247. child: Container(
  248. color: this.makeStickyHeaderBgColor(shrinkOffset),
  249. child: SafeArea(
  250. bottom: false,
  251. child: Container(
  252. height: this.collapsedHeight,
  253. child: Row(
  254. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  255. children: <Widget>[
  256. Container(
  257. margin: EdgeInsets.only(left: 10.px),
  258. child: InkWell(
  259. onTap: () => Navigator.pop(context),
  260. child: CircleAvatar(
  261. radius: 15.px,
  262. backgroundColor: tColor,
  263. child: Icon(
  264. Icons.chevron_left, color: Colors.white,
  265. size: 30.px),
  266. ),
  267. )
  268. ),
  269. Expanded(child: _buildHeaderMenu(shrinkOffset)),
  270. AnimatedSwitcher(
  271. transitionBuilder: (child, anim) =>
  272. ScaleTransition(child: child, scale: anim),
  273. duration: Duration(milliseconds: 300),
  274. child: Container(
  275. key: ValueKey(likeIcon),
  276. padding: EdgeInsets.only(left: 16.px, right: 10.px),
  277. child: InkWell(
  278. onTap: likeClick,
  279. child: CircleAvatar(
  280. radius: 15.px,
  281. backgroundColor: tColor,
  282. child: Icon(
  283. likeIcon, color: Colors.white, size: 21.px),
  284. ),
  285. )
  286. ),
  287. ),
  288. Container(
  289. padding: EdgeInsets.only(left: 16.px, right: 10.px),
  290. child: InkWell(
  291. onTap: _showShare,
  292. child: CircleAvatar(
  293. radius: 15,
  294. backgroundColor: tColor,
  295. child: Icon(Icons.share, color: Colors.white,
  296. size: 21.px),
  297. ),
  298. )
  299. ),
  300. ],
  301. ),
  302. ),
  303. ),
  304. ),
  305. ),
  306. ],
  307. ),
  308. );
  309. }
  310. void _showShare() {
  311. showModalBottomSheet(
  312. context: Cache.context,
  313. shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(
  314. topLeft: Radius.circular(10.px), topRight: Radius.circular(10.px)
  315. )),
  316. builder: (BuildContext context) {
  317. return ShareBar(data);
  318. });
  319. }
  320. Widget _buildHeaderMenu(double shrinkOffset) {
  321. var textColor = this.makeStickyHeaderTextColor(shrinkOffset, false);
  322. return Container(
  323. margin: EdgeInsets.only(left: 20.px, right: 20.px),
  324. child: Row(
  325. crossAxisAlignment: CrossAxisAlignment.center,
  326. mainAxisAlignment: MainAxisAlignment.center,
  327. children: <Widget>[
  328. InkWell(
  329. onTap: () { this.tabIdx = 0; },
  330. child: Container(
  331. width: 60.px,
  332. padding: EdgeInsets.only(left: 6.px, right: 6.px),
  333. child: Text('商品', style: TextStyle(color: textColor)
  334. ,textAlign: TextAlign.center),
  335. ),
  336. ),
  337. InkWell(
  338. onTap: () { this.tabIdx = 2; },
  339. child: Container(
  340. width: 60.px,
  341. padding: EdgeInsets.only(left: 6.px, right: 6.px),
  342. child: Text('评价', style: TextStyle(color: textColor)
  343. ,textAlign: TextAlign.center),
  344. ),
  345. ),
  346. InkWell(
  347. onTap: () { this.tabIdx = 1; },
  348. child: Container(
  349. width: 60.px,
  350. padding: EdgeInsets.only(left: 6.px, right: 6.px),
  351. child: Text('详情', style: TextStyle(color: textColor)
  352. ,textAlign: TextAlign.center),
  353. ),
  354. ),
  355. ],
  356. ),
  357. );
  358. }
  359. }
  360. class ProductContent extends StatelessWidget {
  361. final Details _details;
  362. ProductContent(this._details, {Key key}): super(key: key);
  363. _doSelectAttr (BuildContext context) {
  364. showModalBottomSheet(
  365. context: context,
  366. shape: RoundedRectangleBorder(borderRadius: BorderRadius.only(
  367. topLeft: Radius.circular(10.px), topRight: Radius.circular(10.px)
  368. )),
  369. builder: (BuildContext context) {
  370. return ProductAttrPage(_details, showAll: true);
  371. }
  372. );
  373. }
  374. Widget _buildProductAttr(BuildContext context) {
  375. var attr = this._details?.productAttr;
  376. if(attr != null && attr.length > 0) {
  377. var attrs = "";
  378. for(var item in attr) {
  379. attrs = "$attrs ${item.attr_name}";
  380. }
  381. return Container(
  382. color: Colors.white,
  383. margin: EdgeInsets.only(top: 6.px),
  384. padding: EdgeInsets.only(left: 16.px, right: 10.px, top: 8.px, bottom: 8.px),
  385. child: InkWell(
  386. onTap: () => _doSelectAttr(context),
  387. child: Flex(
  388. direction: Axis.horizontal,
  389. children: <Widget>[
  390. RichText(
  391. text: TextSpan(
  392. text: "选择:",
  393. style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w300, fontSize: 12.px),
  394. children: [
  395. TextSpan(
  396. text: "请选择$attrs",// attr[0].attr_value[0].attr,
  397. style: TextStyle(color: Colors.black, fontSize: 12.px),
  398. ),
  399. ]
  400. ),
  401. ),
  402. Spacer(),
  403. Icon(Icons.keyboard_arrow_right, color: Colors.grey)
  404. ],
  405. )
  406. ),
  407. );
  408. }
  409. return Container();
  410. }
  411. Widget _buildComment (BuildContext context) {
  412. return Container(
  413. color: Colors.white,
  414. margin: EdgeInsets.only(top: 6.px, bottom: 10.px),
  415. padding: EdgeInsets.only(left: 16.px, top: 10.px, right: 10.px, bottom: 10.px),
  416. child: InkWell(
  417. onTap: () {
  418. Navigator.pushNamed(context, RouteNames.comment, arguments: _details.uid.toString());
  419. },
  420. child: Row(
  421. children: [
  422. Text('用户评价(${_details.replyCount})'),
  423. Spacer(),
  424. Text('${_details.replyChance}% ', style: TextStyle(color: Colors.red)),
  425. Text('好评率'),
  426. Icon(Icons.keyboard_arrow_right, color: Colors.grey)
  427. ],
  428. ),
  429. ),
  430. );
  431. }
  432. @override
  433. Widget build(BuildContext context) {
  434. return Container(
  435. color: DColors.back,
  436. child: Column(
  437. children: <Widget>[
  438. Container(
  439. color: Colors.white,
  440. padding: EdgeInsets.only(left: 16.px, top: 16.px),
  441. child: Flex(
  442. direction: Axis.horizontal,
  443. children: <Widget>[
  444. RichText(
  445. text: TextSpan(
  446. text: I18n.$,
  447. style: TextStyle(fontSize: 12.px, color: Colors.red, fontWeight: FontWeight.bold),
  448. children: [
  449. TextSpan(
  450. text: Utils.formatRMB(Cache.isVip ? _details.storeInfo.vip_price : _details.storeInfo.price),
  451. style: TextStyle(fontSize: 20.px, color: DColors.price, fontWeight: FontWeight.bold)
  452. )
  453. ]
  454. ),
  455. ),
  456. Spacer()
  457. ],
  458. )
  459. ),
  460. Container(
  461. color: Colors.white,
  462. width: double.infinity,
  463. padding: EdgeInsets.only(left: 16.px, top: 10.px, right: 16.px),
  464. child: Text(_details.storeInfo.store_name, style: TextStyle(height: 1.3.px, fontWeight: FontWeight.bold))
  465. ),
  466. Container(
  467. color: Colors.white,
  468. padding: EdgeInsets.only(left: 16.px, top: 10.px, right: 16.px, bottom: 10.px),
  469. child: Flex(
  470. direction: Axis.horizontal,
  471. children: <Widget>[
  472. Text('原价:${Utils.formatRMB(_details?.storeInfo?.ot_price, show: true)}', style:
  473. TextStyle(color: Colors.grey, fontWeight: FontWeight.w300, fontSize: 12)),
  474. Spacer(),
  475. Text('库存:${_details?.storeInfo?.stock}', style:
  476. TextStyle(color: Colors.grey, fontWeight: FontWeight.w300, fontSize: 12)),
  477. Spacer(),
  478. Text('销量:${_details?.storeInfo?.fsales}', style:
  479. TextStyle(color: Colors.grey, fontWeight: FontWeight.w300, fontSize: 12))
  480. ],
  481. ),
  482. ),
  483. _buildProductAttr(context),
  484. _buildComment(context),
  485. Container(
  486. color: Colors.white,
  487. padding: EdgeInsets.only(left: 12.px, top: 10.px, right: 12.px),
  488. child: _details?.storeInfo?.description == null ? Container() : Html(data: _details.storeInfo.description),
  489. ),
  490. ],
  491. ),
  492. );
  493. }
  494. }