美文网首页Flutter
Flutter:仿京东商城APP的完整开发指南(三)

Flutter:仿京东商城APP的完整开发指南(三)

作者: 时光啊混蛋_97boy | 来源:发表于2020-12-11 16:18 被阅读0次

    原创:有趣知识点摸索型文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、京东商城APP的添加商品界面
      • 1、顶部Tab条
      • 2、底部加入购物车和结算按钮
      • 3、商品评价
      • 4、商品详情
      • 5、商品属性
      • 6、添加商品界面
    • 二、京东商城APP的结算界面
      • 1、购物车界面点击结算按钮
      • 2、默认收货地址
      • 3、结算商品列表
      • 4、立即下单
    • 三、京东商城APP的地址界面
      • 1、收货地址列表
      • 2、修改收货地址
      • 3、增加收货地址
    • 四、京东商城APP的支付界面
      • 1、支付方式列表的数据源
      • 2、展示支付方式列表
      • 3、点击支付按钮
    • 五、京东商城APP的订单界面
      • 1、顶部导航栏
      • 2、订单列表
    • 六、京东商城APP的订单详情界面
      • 1、收货地址
      • 2、商品列表
      • 3、订单信息
    • Demo
    • 学习资料

    续文见上篇:Flutter:仿京东商城APP的完整开发指南(二)


    一、京东商城APP的添加商品界面

    添加商品

    1、顶部Tab条

    顶部Tab条
    a、Tab位于导航栏位置处
    • TabBarIndicatorSize.label:底部指示器线条和标题等宽
    • indicatorColor:底部指示器线条为红色
    Container(
      // 设置宽度让导航栏三个按钮更加紧凑
      width: ScreenAdaper.width(400),
      child: TabBar(
        // 底部指示器线条为红色
        indicatorColor: Colors.red,
        // 底部指示器线条和标题等宽
        indicatorSize: TabBarIndicatorSize.label,
        tabs: <Widget>[
          Tab(
            child: Text("商品"),
          ),
          Tab(
            child: Text("详情"),
          ),
          Tab(
            child: Text("评价"),
          ),
        ],
      ),
    )
    
    b、下拉菜单
    • showMenu:展示菜单
    • position:菜单放置的位置
    • PopupMenuItem:菜单里面的选项
      弹出下拉菜单
    IconButton(
      icon: Icon(Icons.more_horiz), // ...
      // 弹出下拉菜单
      onPressed: () {
        showMenu(
            context: context,
            position: RelativeRect.fromLTRB(
                ScreenAdaper.width(600), 100, 10, 0),
            items: [
              PopupMenuItem(
                child: Row(
                  // 左图标右文字
                  children: <Widget>[Icon(Icons.home), Text("首页")],
                ),
              ),
              PopupMenuItem(
                child: Row(
                  // 左图标右文字
                  children: <Widget>[Icon(Icons.search), Text("搜索")],
                ),
              )
            ]);
      },
    )
    

    2、底部加入购物车和结算按钮

    a、底部3个按钮的整体布局

    位于底部,成行排列,上面有根线条,左边是购物车点击后跳转到购物车界面,中间是添加购物车按钮,右边是立即购买按钮。

    Positioned(
      width: ScreenAdaper.width(750),
      height: ScreenAdaper.height(88),
      bottom: 0,
      child: Container(
        // 顶部添加一根线条
        decoration: BoxDecoration(
            color: Colors.white,
            border: Border(
                top: BorderSide(
                    color: Colors.black26, width: 1))),
        child: Row(
          // 左购物车 右按钮
          children: <Widget>[
            // 购物车
            InkWell(
              onTap: () {
                Navigator.pushNamed(context, "/cart");
              },
              ......
            ),
    
            // 添加购物车
            Expanded(
                // 自适应
                flex: 1,
                child: JDButton(
                    .......
                )),
    
            // 立即购买
            Expanded(
                // 自适应
                flex: 1,
                child: JDButton(
                    .......
                )),
          ],
        ),
      ),
    )
    
    b、添加购物车按钮

    attr.length > 0如果商品拥有1个以上属性,则需要在弹出的商品属性选择界面选择商品属性后才能成功添加到购物车,否则直接将商品加入到购物车即可,加入后需要及时更新购物车界面中的商品列表数据,再告知用户加入购物车成功。

    child: JDButton(
      buttonColor: Color.fromRGBO(253, 1, 0, 0.9),
      buttonTitle: "加入购物车",
      // 事件广播,在其他页面调用方法
      tapEvent: () async {
        if (this._productContentList[0].attr.length > 0) {
          // 弹出筛选框
          eventBus.fire(new ProductContentEvent("加入购物车"));
        } else {
          // 调用加入购物车服务改变数据
          await CartService.addCart(this._productContentList[0]);
    
          // 先加入购物车后再更新数据
          this._cartProvider.updateCartList();
    
          // 弹出提示框
          Fluttertoast.showToast( msg: "加入购物车成功", toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER);
        }
      },
    )
    
    c、立即购买按钮

    同样点击立即购买的时候也需要判断是否商品存在属性,若存在则也需要弹出选择属性界面,否则直接购买。

    child: JDButton(
      buttonColor: Color.fromRGBO(255, 165, 0, 0.9),
      buttonTitle: "立即购买",
      tapEvent: () {
        // 是否有属性
        if (this
                ._productContentList[0]
                .attr
                .length >
            0) {
          // 弹出筛选框
          eventBus.fire(
              new ProductContentEvent("立即购买"));
        } else {
          print("立即购买");
        }
      },
    )),
    

    3、商品评价

    评价

    本人有点儿懒,所以不想写......

    class _ProductContentRatePageState extends State<ProductContentRatePage> {
      @override
      Widget build(BuildContext context) {
        return Container(
          child: ListView.builder(
            itemCount: 30,
            itemBuilder: (context,index) {
              return ListTile(
                title: Text("第${index}条数据"),
              );
            },
          ),
        );
      }
    }
    

    4、商品详情

    商品详情
    a、获取传入的商品id
    InAppWebViewController webView;
    String url = "";
    double progress = 0;
    var _id;
    
    @override
    void initState() {
      // TODO: implement initState
      super.initState();
      this._id = widget._productContentList[0].sId;
    }
    
    b、边框

    在商品详情信息外边加了一个蓝色边框。

    child: Container(
      margin: const EdgeInsets.all(10.0),
      decoration:
      BoxDecoration(border: Border.all(color: Colors.blueAccent)),
      child: InAppWebView(
        .......
      ),
    ),
    
    c、获取网页版的商品详情

    导入加载网页依赖库

    flutter_inappwebview: ^2.1.0+1
    import 'package:flutter_inappwebview/flutter_inappwebview.dart';
    
    • initialUrl:根据商品id从网页获取其详情信息
    • onLoadStart:根据url开始加载网页
    • onLoadStop:加载结束
    • onProgressChanged:进度条
    child: InAppWebView(
      initialUrl: "http://jd.itying.com/pcontent?id=${this._id}",
      initialHeaders: {},
    
      initialOptions: InAppWebViewWidgetOptions(
          inAppWebViewOptions: InAppWebViewOptions(
            debuggingEnabled: true,
          )
      ),
    
      onWebViewCreated: (InAppWebViewController controller) {
        webView = controller;
      },
      onLoadStart: (InAppWebViewController controller, String url) {
        setState(() {
          this.url = url;
        });
      },
      onLoadStop: (InAppWebViewController controller, String url) async {
        setState(() {
          this.url = url;
        });
      },
      onProgressChanged: (InAppWebViewController controller, int progress) {
        setState(() {
          this.progress = progress / 100;
        });
      },
    ),
    

    5、商品属性

    商品属性
    a、后端接口传入的JSON数据
    [
    
       {
        "cate":"尺寸",
        list":[{
    
             "title":"xl",
             "checked":false
           },
           {
    
             "title":"xxxl",
             "checked":true
           },
         ]
       },
       {
        "cate":"颜色",
        list":[{
    
             "title":"黑色",
             "checked":false
           },
           {
    
             "title":"白色",
             "checked":true
           },
         ]
       }
    ]
    
    b、请求详情页面数据 Model
    List _productContentList = [];
    _getDetailData() async {
      var api = "${Config.domain}api/pcontent?id=${widget.arguments["id"]}";
      var result = await Dio().get(api);
      var proudctDetailModel = new ProductContentMainModel.fromJson(result.data);
      setState(() {
        this._productContentList.add(proudctDetailModel.result);
      });
    }
    
    c、初始化属性列表
    • attr.length:指的是总共有几项属性,比如这里的鞋子就有“鞋面材料、闭合方式、颜色这3项属性
    • attr[i].list.length:指的每项属性里面又提供几种具体的表现类型,比如说鞋子颜色就分为红色、白色、黄色
    • "title": attr[i].list[j]:表现类型的标题
    • "checked": true:表现类型是否被选中,第一个表现类型为默认选中
    • attr[i].attrList.clear():防止多次调用,清空之前旧数据再添加
    _initAttr() {
      List attr = this._attr;
      for (var i = 0; i < attr.length; i++) {
        // 防止多次调用,清空之前旧数据再添加
        attr[i].attrList.clear();
    
        for (var j = 0; j < attr[i].list.length; j++) {
          if (j == 0) { // 第一条为默认选中
            attr[i].attrList.add({"title": attr[i].list[j], "checked": true});
          } else {
            attr[i].attrList.add({"title": attr[i].list[j], "checked": false});
          }
        }
      }
    
      this._attr = attr;
    
      // 获取选中值
      _getSelectedAttrValue();
    }
    

    获取选中的属性值。遍历每项属性的所有表现类型,倘若其被选中则将其标题添加到临时数组中去,最后再将临时数组中所有被选中的标题用""连接起来成为字符串传递给this._productContent.selectedAttr

    _getSelectedAttrValue() {
      var _list = this._attr;
      List tempArr = [];
      for (var i = 0; i < _list.length; i++) {
        for (var j = 0; j < _list[i].attrList.length; j++) {
          if (_list[i].attrList[j]['checked'] == true) {
            tempArr.add(_list[i].attrList[j]["title"]);
          }
        }
      }
    
      setState(() {
        this._selectedValue = tempArr.join(',');
    
        // 给选中的属性赋予值
        this._productContent.selectedAttr = this._selectedValue;
      });
    }
    
    d、渲染每个属性按钮
    • attrItemList.add:循环向组件列表中添加每个属性按钮
    • onTap:点击属性按钮后切换选中的属性
    • Chip:tag样式的组件,选中背景为红色字体为白色
    List<Widget> _getAttrItemWidget(attrItem, setBottomState) {
      List<Widget> attrItemList = [];
      attrItem.attrList.forEach((item) {
        attrItemList.add(Container(
          margin: EdgeInsets.all(10),
          child: InkWell(
            onTap: () {
              // 切换选中Tag
              _changeAttr(attrItem.cate, item["title"], setBottomState);
            },
            child: Chip(
              // Tag属性
              label: Text("${item["title"]}",
                  style: TextStyle(
                      color: item["checked"] ? Colors.white : Colors.black54)),
              padding: EdgeInsets.all(10),
              backgroundColor: item["checked"] ? Colors.red : Colors.black26,
            ),
          ),
        ));
      });
      return attrItemList;
    }
    
    e、渲染属性选择面板

    左边为每项属性的标题,右边为每项属性的表现类型。需要注意的是右侧内容可能较多,要防止溢出。

    List<Widget> _getAttrWidget(setBottomState) {
      List<Widget> attrList = [];
      this._attr.forEach((attrItem) {
        attrList.add(Wrap(
          // 右侧内容较多,防止溢出
          children: <Widget>[
            // Container 用来调整text和wrap的左右位置和宽度
            Container(
              width: ScreenAdaper.width(120),
              child: Padding(
                padding: EdgeInsets.only(top: ScreenAdaper.height(22)),
                child: Text("${attrItem.cate}: ",
                    style: TextStyle(fontWeight: FontWeight.bold)),
              ),
            ),
            Container(
              width: ScreenAdaper.width(590),
              child: Wrap(
                children: _getAttrItemWidget(attrItem, setBottomState),
              ),
            )
          ],
        ));
      });
    
      return attrList;
    }
    
    f、渲染商品属性选择面板
    • StatefulBuilder:由于弹出的商品属性选择界面和之前的添加商品界面不是一个界面,所以需要使用StatefulBuilder可以保持两个界面的状态同步
    • GestureDetector:InkWell 自带水墨纹点击效果,GestureDetector则没有
    • 点击穿透问题:需要避免点击选择面板的空白区域也会导致属性面板消失的问题
    • Stack:商品属性选择面板
    • Positioned:底部的加入购物车和立即购买按钮
    _attrBottomSheet() {
       showModalBottomSheet(
           context: context,
           builder: (contex) {
             return StatefulBuilder(
               builder: (BuildContext context, setBottomState) {
                 // InkWell 自带水墨纹点击效果,GestureDetector没有
                 return GestureDetector(
                   // 点击穿透问题:禁止
                   behavior: HitTestBehavior.opaque,
    
                   //解决showModalBottomSheet点击消失的问题
                   onTap: () {
                     return false;
                   },
                   child: Stack(
                     children: <Widget>[
                       // 选项
                       Container(
                         padding: EdgeInsets.all(ScreenAdaper.width(20)),
                         child: ListView(
                           children: <Widget>[
                            ......
                           ],
                         ),
                       ),
    
                       // 底部按钮
                       Positioned(
                         bottom: 0,
                         width: ScreenAdaper.width(750),
                         height: ScreenAdaper.height(76),
                         child: Row(
                           children: <Widget>[
                            .....
                           ],
                         ),
                       )
                     ],
                   ),
                 );
               },
             );
           });
     }
    

    布局为右边是取消按钮,点击后返回到添加商品页面。中间是商品属性选项。下方是商品数量的增减。

    children: <Widget>[
       // 取消按钮
       Align(
         alignment: Alignment.centerRight,
         child: InkWell(
           child: Icon(Icons.cancel),
           onTap: () {
             Navigator.pop(context);
           },
         ),
       ),
    
       // 选项
       Column(
           mainAxisAlignment: MainAxisAlignment.center,
           children: _getAttrWidget(setBottomState)),
    
       // 数量增减
       Divider(),
       Container(
         margin: EdgeInsets.only(top: 10),
         height: ScreenAdaper.height(80),
         child: Row(
           children: <Widget>[
             Text("数量: ", style: TextStyle(fontWeight: FontWeight.bold)),
             // 左右加点间距
             SizedBox(width: 10),
             ProductContentCartNum(this._productContent)
           ],
         ),
       )
     ],
    

    左侧-号按钮最多减为1。

    Widget _leftBtn() {
      return InkWell(
        onTap: () {
          if (this._productContent.count > 1) {
            setState(() {
              this._productContent.count = this._productContent.count - 1;
            });
          }
        },
        child: Container(
          alignment: Alignment.center,
          width: ScreenAdaper.width(45),
          height: ScreenAdaper.height(45),
          child: Text("-"),
        ),
      );
    }
    

    右侧+号按钮

    Widget _rightBtn() {
      return InkWell(
        onTap: (){
          setState(() {
            this._productContent.count++;
          });
        },
        child: Container(
          alignment: Alignment.center,
          width: ScreenAdaper.width(45),
          height: ScreenAdaper.height(45),
          child: Text("+"),
        ),
      );
    }
    

    中间区域显示商品的数量。

    Widget _centerArea() {
      return Container(
        alignment: Alignment.center,
        width: ScreenAdaper.width(70),
        decoration: BoxDecoration(
            border: Border(
              left: BorderSide(width: 1, color: Colors.black12),
              right: BorderSide(width: 1, color: Colors.black12),
            )),
        height: ScreenAdaper.height(45),
        child: Text("${this._productContent.count}"),
      );
    }
    

    加入购物车按钮。

    1. 调用加入购物车服务改变数据
    2. 弹出页面消失
    3. 加入购物车后需要更新购物车界面的数据
    4. 弹出"加入购物车成功"提示框
    child: JDButton(
      buttonTitle: "加入购物车",
      buttonColor: Color.fromRGBO(253, 1, 0, 0.9),
      tapEvent: () async {
        // 调用加入购物车服务改变数据
        await CartService.addCart(
            this._productContent);
    
        // 弹出页面消失
        Navigator.pop(context);
        // Navigator.of(context).pop();
    
        // 先加入购物车后再更新数据
        this._cartProvider.updateCartList();
    
        // 弹出提示框
        Fluttertoast.showToast(
            msg: "加入购物车成功",
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER);
      },
    ),
    

    立即购买按钮。由于没有做购买服务,所以这里只是让弹出页面消失。

    child: JDButton(
      buttonTitle: "立即购买",
      buttonColor: Color.fromRGBO(253, 165, 0, 0.9),
      tapEvent: () {
        // 弹出页面消失
        Navigator.pop(context);
        // Navigator.of(context).pop();
      },
    ),
    

    6、添加商品界面

    添加商品界面
    a、商品图片
    AspectRatio(
      aspectRatio: 16 / 16,
      child: Image.network("${pic}", fit: BoxFit.cover),
    ),
    
    b、商品标题
    Container(
      padding: EdgeInsets.only(top: 10),
      child: Text("${this._productContent.title}",
          style: TextStyle(
              color: Colors.black87,
              fontSize: ScreenAdaper.fontSize(36))),
    ),
    
    c、商品副标题
    Container(
        padding: EdgeInsets.only(top: 10),
        child: Text("${this._productContent.subTitle}",
            style: TextStyle(
                color: Colors.black54,
                fontSize: ScreenAdaper.fontSize(28)))),
    
    d、商品价格
    Container(
      padding: EdgeInsets.only(top: 10),
      child: Row(
        children: <Widget>[
          // 均匀分配
          Expanded(
            flex: 1,
            child: Row(
              children: <Widget>[
                Text("特价: "),
                Text("¥${this._productContent.price}",
                    style: TextStyle(
                        color: Colors.red,
                        fontSize: ScreenAdaper.fontSize(46))),
              ],
            ),
          ),
          Expanded(
            flex: 1,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: <Widget>[
                Text("原价: "),
                Text("¥${this._productContent.oldPrice}",
                    style: TextStyle(
                        color: Colors.black38,
                        fontSize: ScreenAdaper.fontSize(28),
                        decoration: TextDecoration.lineThrough)),
              ],
            ),
          )
        ],
      ),
    ),
    
    e、商品属性

    点击后弹出商品属性选择界面。当没有筛选项目时候不显示。

    this._attr.length > 0
        ? Container(
            margin: EdgeInsets.only(top: 10),
            height: ScreenAdaper.height(80),
            child: InkWell(
              onTap: () {
                _attrBottomSheet();
              },
              child: Row(
                children: <Widget>[
                  Text("已选: ",
                      style: TextStyle(fontWeight: FontWeight.bold)),
                  Text("${this._selectedValue}")
                ],
              ),
            ),
          )
        : Text(""),
    
    e、运费

    哈哈,通通免运费!

    Container(
      height: ScreenAdaper.height(80),
      child: Row(
        children: <Widget>[
          Text("运费: ", style: TextStyle(fontWeight: FontWeight.bold)),
          Text("免运费")
        ],
      ),
    ),
    

    二、京东商城APP的结算界面

    结算界面

    1、购物车界面点击结算按钮

    结算按钮
    a、获取购物车选中的数据

    先获取到购物车中所有的商品列表,再对其进行遍历,如过商品有被选中则将其添加到选中列表中去。

    static getCheckOutData() async {
      // 全部数据
      List cartListData = [];
      try {
        cartListData = json.decode(await Storage.getString('cartList'));
      } catch (e) {
        cartListData = [];
      }
    
      // 筛选选中数据
      List tempCheckOutData = [];
      for (var i = 0; i < cartListData.length; i++) {
        if (cartListData[i]["checked"] == true) {
          tempCheckOutData.add(cartListData[I]);
        }
      }
      return tempCheckOutData;
    }
    
    b、保存购物车选中数据
    CheckOutProvider _checkOutProvider;
    
    // 点击结算按钮
    doCheckOut() async {
      // 获取结算数据
      List checkOutData = await CartService.getCheckOutData();
      // 保存选中数据
      this._checkOutProvider.changeCheckOutListData(checkOutData);
      ......
    }
    
    c、跳转界面

    如果购物车没有选中的数据则提示"没有选中商品哦",如果有数据则再判断用户是否登陆,未登录就提示"请先登陆再结算",若已经登录则跳转到“结算”界面。

    doCheckOut() async {
      // 购物车是否有选中的数据
      if (checkOutData.length > 0) {
        // 判断用户是否登陆
        bool userLoginState = await UserServices.getUserState();
        if (userLoginState) {
          Navigator.pushNamed(context, "/checkOut");
        } else {
          Fluttertoast.showToast( msg: "请先登陆再结算", toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER);
          Navigator.pushNamed(context, "/login");
        }
      } else {
        Fluttertoast.showToast( msg: "没有选中商品哦", toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER);
      }
    }
    

    2、默认收货地址

    默认收货地址
    a、获取用户地址列表

    JSON字符串

    Map addressListAttr={
      "uid":'1',
      "age":10,
      "salt":'xxxxxxxxxxxxxx'  //私钥
    };
    

    获取私钥的算法

    class SignServices {
      static getSign(json) {
    //    Map addressListAttr={
    //      "uid":'1',
    //      "age":10,
    //      "salt":'xxxxxxxxxxxxxx'  //私钥
    //    };
        List attrKeys = json.keys.toList();
        attrKeys.sort();
    
        String str='';
        for(var i = 0; i < attrKeys.length; i++){
          str += "${attrKeys[i]}${json[attrKeys[i]]}";
        }
        return md5.convert(utf8.encode(str)).toString();
      }
    }
    

    根据uidsign请求接口获取登录用户的地址列表

    _getDefaultAddress() async {
      List userinfo = await UserServices.getUserInfo();
      var tempJson = {
        "uid": userinfo[0]["_id"],
        "salt": userinfo[0]["salt"]
      };
      var sign = SignServices.getSign(tempJson);
      var api = '${Config.domain}api/oneAddressList?uid=${userinfo[0]["_id"]}&sign=${sign}';
      var response = await Dio().get(api);
      setState(() {
        this._addressList=response.data['result'];
      });
    }
    

    监听默认地址改变的广播

    eventBus.on<DefaultAddressEvent>().listen((event) {
      print(event.str);
      this._getDefaultAddress();
    });
    
    b、默认收货地址界面

    当地址列表为空的时候显示"请添加收获地址",点击后跳转到添加地址界面。当地址列表中存在地址的时候则显示用户名+手机号及其默认地址。这里默认地址取的是地址列表中的第一个地址。

    this._addressList.length == 0 ? ListTile(
      leading: Icon(Icons.add_location),
      title: Center(
        // 使文本居中
        child: Text("请添加收获地址"),
      ),
      trailing: Icon(Icons.navigate_next),
      onTap: () {
        Navigator.pushNamed(context, "/addressAdd");
      },
    ) : ListTile(
      title: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text("${this._addressList[0]["name"]}  ${this._addressList[0]["phone"]}"),
          SizedBox(height: 10),
          Text("${this._addressList[0]["address"]}"),
        ],
      ),
      trailing: Icon(Icons.navigate_next),
      onTap: () {
        Navigator.pushNamed(context, '/addressList');
      },
    ),
    

    3、结算商品列表

    结算商品列表
    a、通过Provider共享状态获取到购物车中的商品数据
    this._checkOutProvider = Provider.of<CheckOutProvider>(context);
    
    class CheckOutProvider with ChangeNotifier {
      List _checkOutListData = []; 
      List get checkOutListData => this._checkOutListData;
    
      changeCheckOutListData(data){
        this._checkOutListData = data;
        notifyListeners();
      }
    }
    
    b、遍历获取到的购物车中的商品数据来进行渲染显示结算商品列表

    结算商品列表的视图布局_checkOutItem和购物车部分类似,就不再赘述了。

    Container(
      padding: EdgeInsets.all(ScreenAdaper.width(20)),
      child: Column(
        children: this._checkOutProvider.checkOutListData.map((item){
          return Column(
            children: <Widget>[
              _checkOutItem(item),
              Divider()
            ],
          );
        }).toList()
      ),
    ),
    
    c、结算账单
    Container(
      color: Colors.white,
      padding: EdgeInsets.all(ScreenAdaper.width(20)),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text("商品总金额:¥100"),
          Divider(),
          Text("立减:¥5"),
          Divider(),
          Text("运费:¥0"),
        ],
      ),
    ),
    

    4、立即下单

    立即下单
    a、计算总价
    static getAllPrice(checkOutListData) {
      var tempAllPrice=0.0;
      for (var i = 0; i < checkOutListData.length; i++) {
        if (checkOutListData[i]["checked"] == true) {
          tempAllPrice += checkOutListData[i]["price"] * checkOutListData[i]["count"];
        }
      }
      return tempAllPrice;
    }
    

    商品总价保留一位小数

    var allPrice = CheckOutServices.getAllPrice(_checkOutProvider.checkOutListData).toStringAsFixed(1);
    
    b、立即下单按钮样式
    Align(
      alignment: Alignment.centerRight,
      child: RaisedButton(
        child:
        Text('立即下单', style: TextStyle(color: Colors.white)),
        color: Colors.red,
        onPressed: () async {
            ......
        },
      ),
    )
    
    c、获取请求的签名并进行支付请求

    签名算法之前在讲默认地址的时候提到过就不再赘述了。

    List userinfo = await UserServices.getUserInfo();
    
    var sign = SignServices.getSign({
      "uid": userinfo[0]["_id"],
      "phone": this._addressList[0]["phone"],
      "address": this._addressList[0]["address"],
      "name": this._addressList[0]["name"],
      "all_price":allPrice,
      "products": json.encode(_checkOutProvider.checkOutListData),
      "salt":userinfo[0]["salt"]   //私钥
    });
    

    请求接口的时候传入了一大堆支付需要用到的参数。

    var response = await Dio().post(api, data: {
      "uid": userinfo[0]["_id"],
      "phone": this._addressList[0]["phone"],
      "address": this._addressList[0]["address"],
      "name": this._addressList[0]["name"],
      "all_price":allPrice,
      "products": json.encode(_checkOutProvider.checkOutListData),
      "sign": sign
    });
    
    d、支付状态

    如果支付成功了则需要删除购物车中选中的商品数据,再调用CartProvider更新购物车数据,最后跳转到支付页面。如果支付未能成功,比如收货地址未填写,则提示之,当然还有其他可能的错误原因,这里只列举了一种。

    if(response.data["success"]){
      //删除购物车选中的商品数据
      await CheckOutServices.removeUnSelectedCartItem();
    
      //调用CartProvider更新购物车数据
      cartProvider.updateCartList();
    
      //跳转到支付页面
      Navigator.pushNamed(context, '/pay');
    } else {
    Fluttertoast.showToast( msg: "请先填写收获地址", toastLength: Toast.LENGTH_SHORT,gravity: ToastGravity.CENTER);
    }
    

    删除购物车中选中的商品数据的方法如下。暂存所有未选中的商品再将其返回即可。

    static removeUnSelectedCartItem() async{
      List _cartList=[];
      List _tempList=[];
    
      //获取购物车的数据
      try {
        List cartListData = json.decode(await Storage.getString('cartList'));
        _cartList = cartListData;
      } catch (error) {
        _cartList = [];
      }
    
      for (var i = 0; i < _cartList.length; i++) {
        if (_cartList[i]["checked"] == false) {
           _tempList.add(_cartList[I]);
        }
      }
    
      Storage.setString("cartList", json.encode(_tempList));
    }
    

    三、京东商城APP的地址界面

    1、收货地址列表

    收货地址列表
    a、请求地址列表数据

    请求地址列表接口的实现

    _getAddressList() async {
      //请求接口
      List userinfo = await UserServices.getUserInfo();
      var tempJson = {"uid": userinfo[0]['_id'], "salt": userinfo[0]["salt"]};
      var sign = SignServices.getSign(tempJson);
      var api = '${Config.domain}api/addressList?uid=${userinfo[0]['_id']}&sign=${sign}';
      var response = await Dio().get(api);
      setState(() {
        this.addressList = response.data["result"];
      });
    }
    

    调用请求接口获取数据

    List addressList = [];
    @override
    void initState() {
      super.initState();
      this._getAddressList();
    
      // 监听增加收货地址的广播
      eventBus.on<AddressEvent>().listen((event) {
        print(event.str);
        this._getAddressList();
      });
    }
    
    b、使用地址列表数据渲染界面
    • leading:假如是默认地址,则在前面显示红色的☑️
    • onTap:单击可以修改默认地址
    • onLongPress:长按可以删除收获地址
    • trailing:尾部的编辑按钮按下后可以跳转到编辑地址界面
    ListView.builder(
      itemCount: this.addressList.length,
      itemBuilder: (context,index) {
        return Column(
          children: <Widget>[
            SizedBox(height: 20),
            ListTile(
              leading: this.addressList[index]["default_address"] == 1 ? Icon(Icons.check, color: Colors.red) : null,
              title: InkWell(
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text("${this.addressList[index]["name"]} ${this.addressList[index]["phone"]}"),
                      SizedBox(height: 10),
                      Text("${this.addressList[index]["address"]}"),
                    ]
                ),
                onTap: () {
                  // 修改默认地址
                  _changeDefaultAddress(this.addressList[index]["_id"]);
                },
                onLongPress: () {
                  // 删除收获地址
                  _showDelAlertDialog(this.addressList[index]["_id"]);
                },
              ),
              trailing: IconButton(
                icon:Icon(Icons.edit, color: Colors.blue),
                onPressed: (){
                  Navigator.pushNamed(context, '/addressEdit',arguments: {
                    "id":this.addressList[index]["_id"],
                    "name":this.addressList[index]["name"],
                    "phone":this.addressList[index]["phone"],
                    "address":this.addressList[index]["address"],
                  });
                },
              ),
            ),
            Divider(height: 20)
          ],
        );
      },
    ),
    

    修改默认收货地址。传入用户id、地址idsalt作为参数请求修改默认地址的接口。请求完成后跳转到结算界面,此时结算界面显示的默认地址即时修改后的地址,再点击该地址进入到收货地址列表,默认地址总是排在首个位置。

    _changeDefaultAddress(id) async{
      List userinfo = await UserServices.getUserInfo();
      var tempJson = {"uid": userinfo[0]['_id'], "id":id,"salt": userinfo[0]["salt"]};
      var sign = SignServices.getSign(tempJson);
      var api = '${Config.domain}api/changeDefaultAddress';
      var response = await Dio().post(api,data:{
        "uid": userinfo[0]['_id'],
        "id":id,
        "sign":sign
      });
      Navigator.pop(context);
    }
    

    监听默认地址改变的事件

    eventBus.on<DefaultAddressEvent>().listen((event) {
      print(event.str);
      this._getDefaultAddress();
    });
    

    页面销毁时发送修改收货地址的事件

    dispose(){
      super.dispose();
      eventBus.fire(new DefaultAddressEvent('修改收货地址成功...'));
    }
    

    在弹出框中点击删除收货地址。传入用户id、地址idsalt作为参数请求删除地址接口。删除收货地址完成后重新获取地址列表。

    _delAddress(id) async{
      List userinfo = await UserServices.getUserInfo();
      var tempJson = {
        "uid":userinfo[0]["_id"],
        "id":id,
        "salt":userinfo[0]["salt"]
      };
      var sign=SignServices.getSign(tempJson);
      var api = '${Config.domain}api/deleteAddress';
      var response = await Dio().post(api,data:{
        "uid":userinfo[0]["_id"],
        "id":id,
        "sign":sign
      });
      //删除收货地址完成后重新获取列表
      this._getAddressList();
    }
    

    2、修改收货地址

    修改收货地址
    a、初始化的时候给编辑页面赋值
    TextEditingController nameController=new TextEditingController();
    TextEditingController phoneController=new TextEditingController();
    TextEditingController addressController=new TextEditingController();
    
    void initState() {
      // TODO: implement initState
      super.initState();
    
      nameController.text = widget.arguments['name'];
      phoneController.text = widget.arguments['phone'];
      addressController.text = widget.arguments['address'];
    }
    
    b、输入框
    JdText(
      controller: nameController,
      text: "收货人姓名",
      onChanged: (value){
        nameController.text = value;
      },
    ),
    
    JdText(
      controller: phoneController,
      text: "收货人电话",
      onChanged: (value){
        phoneController.text = value;
      },
    ),
    
    JdText(
      controller: addressController,
      text: "详细地址",
      maxLines: 4,
      height: 200,
      onChanged: (value){
        addressController.text = value;
      },
    ),
    
    c、弹出地址选择器

    导入城市选择器的依赖库

    city_pickers: ^0.1.30
    import 'package:city_pickers/city_pickers.dart';
    

    倘若传入的地址存在则显示该地址,否则显示默认的'省/市/区'文本。点击选择器后调用CityPickers.showCityPicker展示选择器,再获取到从选择器中选中的地址将其展示出来。

    child: InkWell(
      child: Row(
        children: <Widget>[
          Icon(Icons.add_location),
          this.area.length>0?Text('${this.area}', style: TextStyle(color: Colors.black54)):Text('省/市/区', style: TextStyle(color: Colors.black54))
        ],
      ),
      onTap: () async{
        Result result = await CityPickers.showCityPicker(
            context: context,
            // 可以直接定位
            // locationCode: "130102",
            cancelWidget:
            Text("取消", style: TextStyle(color: Colors.blue)),
            confirmWidget:
            Text("确定", style: TextStyle(color: Colors.blue))
        );
        setState(() {
          this.area= "${result.provinceName}/${result.cityName}/${result.areaName}";
        });
      },
    ),
    
    d、修改地址按钮

    请求修改地址接口,修改完成后跳转到收货地址列表。

    JDButton(buttonTitle: "修改", buttonColor: Colors.red,tapEvent: () async{
      List userinfo=await UserServices.getUserInfo();
      var tempJson={
        "uid":userinfo[0]["_id"],
        "id":widget.arguments["id"],
        "name": nameController.text,
        "phone":phoneController.text,
        "address":addressController.text,
        "salt":userinfo[0]["salt"]
      };
      var sign = SignServices.getSign(tempJson);
      var api = '${Config.domain}api/editAddress';
      var response = await Dio().post(api,data:{
        "uid":userinfo[0]["_id"],
        "id":widget.arguments["id"],
        "name": nameController.text,
        "phone":phoneController.text,
        "address":addressController.text,
        "sign":sign
      });
      Navigator.pop(context);
    })
    

    页面销毁时发出修改地址的事件

    dispose(){
      super.dispose();
      eventBus.fire(new AddressEvent('编辑成功...'));
    }
    

    收货地址列表接收到该事件后即会重新请求最新的地址列表数据。

    eventBus.on<AddressEvent>().listen((event) {
      print(event.str);
      this._getAddressList();
    });
    

    3、增加收货地址

    a、增加收货地址按钮
    增加收货地址按钮

    点击后跳转到增加收货地址页面。

    child: InkWell(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.add, color: Colors.white),
          Text("增加收货地址", style: TextStyle(color: Colors.white))
        ],
      ),
      onTap: (){
        Navigator.pushNamed(context,'/addressAdd');
      },
    ),
    
    b、增加收货地址页面

    基本布局和修改收货地址相同,就不过多描述了。区别包括刚进入时候各文本框值为空,使用的是增加地址接口,增加完成后改地址设为默认收货地址。

    增加收货地址页面

    四、京东商城APP的支付界面

    支付界面

    1、支付方式列表的数据源

      List payList = [
        {
          "title": "支付宝支付",
          "chekced": true,
          "image": "https://www.itying.com/themes/itying/images/alipay.png"
        },
        {
          "title": "微信支付",
          "chekced": false,
          "image": "https://www.itying.com/themes/itying/images/weixinpay.png"
        }
      ];
    

    2、展示支付方式列表

    从左到右依次是支付方式的图标和文本、是否选中的图标。由于只能选择一种支付方式,所以当点击后可以将所有支付方式全部置为未选中,再将点击的那项置为选中,造成单选效果。

    child: ListView.builder(
      itemCount: this.payList.length,
      itemBuilder: (context, index) {
        return Column(
          children: <Widget>[
            ListTile(
              leading: Image.network("${this.payList[index]["image"]}"),
              title: Text("${this.payList[index]["title"]}"),
              trailing: this.payList[index]["chekced"] ? Icon(Icons.check) : Text(""),
    
    
              onTap: () {
                setState(() {
                  //让payList里面的checked都等于false
                  for (var i = 0; i < this.payList.length; i++) {
                    this.payList[i]['chekced'] = false;
                  }
                  // 当前选中的true
                  this.payList[index]["chekced"] = true;
                });
              },
            ),
            Divider(),
          ],
        );
      },
    ),
    

    3、点击支付按钮

    点击后即可支付1111元,逗你的。

    JDButton(
      buttonTitle: "支付",
      buttonColor: Colors.red,
      height: 74,
      tapEvent: () {
        print('支付1111');
      },
    )
    

    五、京东商城APP的订单界面

    订单界面

    1、顶部导航栏

    顶部导航栏

    只是作展示用,不支持点击换页和滚动换页。

    Positioned(
      top: 0,
      width: ScreenAdaper.width(750),
      height: ScreenAdaper.height(76),
      child: Container(
        width: ScreenAdaper.width(750),
        height: ScreenAdaper.height(76),
        color: Colors.white,
        child: Row(
          children: <Widget>[
            Expanded(
              child: Text("全部", textAlign: TextAlign.center),
            ),
            Expanded(
              child: Text("待付款", textAlign: TextAlign.center),
            ),
            Expanded(
              child: Text("待收货", textAlign: TextAlign.center),
            ),
            Expanded(
              child: Text("已完成", textAlign: TextAlign.center),
            ),
            Expanded(
              child: Text("已取消", textAlign: TextAlign.center),
            )
          ],
        ),
      ),
    )
    

    2、订单列表

    订单列表
    a、获取订单列表的数据
    // 获取数据
    List _orderList = [];
    @override
    void initState() {
      // TODO: implement initState
      super.initState();
      this._getListData();
    }
    void _getListData() async {
      List userinfo = await UserServices.getUserInfo();
      var tempJson = {"uid": userinfo[0]['_id'], "salt": userinfo[0]["salt"]};
      var sign = SignServices.getSign(tempJson);
      var api = '${Config.domain}api/orderList?uid=${userinfo[0]['_id']}&sign=${sign}';
      var response = await Dio().get(api);
      print(response.data is Map);
      setState(() {
        var orderMode = new OrderModel.fromJson(response.data);
        this._orderList = orderMode.result;
        print(this._orderList[0].name);
      });
    }
    
    b、整个订单的展示方式

    点击订单后进入到详情界面。左边显示订单价格的合计,右边显示申请售后按钮。

    children: this._orderList.map((value) {
      return InkWell(
        onTap: (){
          Navigator.pushNamed(context, '/orderinfo');
        },
        child: Card(
          child: Column(
            children: <Widget>[
              ListTile(
                title: Text("订单编号:${value.sId}",
                    style: TextStyle(color: Colors.black54)),
              ),
              Divider(),
              Column(
                children: this._orderItemWidget(value.orderItem),
              ),
              SizedBox(height: 10),
              ListTile(
                leading: Text("合计:¥${value.allPrice}"),
                trailing: FlatButton(
                  child: Text("申请售后"),
                  onPressed: () {},
                  color: Colors.grey[100],
                ),
              ),
            ],
          ),
        ),
      );
    }).toList()),
    
    c、每个订单中的商品列表展示方式

    中间部分为订单中商品列表。通过遍历将订单中每个商品的标题、图片、数量都加入到临时组件数组中,再将组件数组返回即为需要展示的订单商品列表。

    List<Widget> _orderItemWidget(orderItems) {
      List<Widget> tempList = [];
      for (var i = 0; i < orderItems.length; i++) {
        tempList.add(Column(
          children: <Widget>[
            SizedBox(height: 10),
            ListTile(
              leading: Container(
                width: ScreenAdaper.width(120),
                height: ScreenAdaper.height(120),
                child: Image.network(
                  '${orderItems[i].productImg}',
                  fit: BoxFit.cover,
                ),
              ),
              title: Text("${orderItems[i].productTitle}"),
              trailing: Text('x${orderItems[i].productCount}'),
            ),
            SizedBox(height: 10)
          ],
        ));
      }
      return tempList;
    }
    

    六、京东商城APP的订单详情界面

    订单详情界面

    1、收货地址

    只是作为展示用,用的是假数据,并没有添加逻辑功能。

    Container(
      color: Colors.white,
      child: Column(
        children: <Widget>[
          SizedBox(height: 10),
          ListTile(
            leading: Icon(Icons.add_location),
            title: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text("张三  15201686455"),
                SizedBox(height: 10),
                Text("北京市海淀区 西二旗"),
              ],
            ),
          ),
          SizedBox(height: 10),
        ],
      ),
    ),
    

    2、商品列表

    只是用假数据展示了下布局方式。

    Row(
      children: <Widget>[
        Container(
          margin: EdgeInsets.fromLTRB(0, 10, 0, 0),
          width: ScreenAdaper.width(120),
          child: Image.network(
              "https://www.itying.com/images/flutter/list2.jpg",
              fit: BoxFit.cover),
        ),
        Expanded(
            flex: 1,
            child: Container(
              padding: EdgeInsets.fromLTRB(10, 10, 10, 5),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text("四季沐歌 (MICOE) 洗衣机水龙头 洗衣机水嘴 单冷快开铜材质龙头",
                      maxLines: 2,
                      style: TextStyle(color: Colors.black54)),
                  Text("水龙头 洗衣机",
                      maxLines: 2,
                      style: TextStyle(color: Colors.black54)),
                  ListTile(
                    leading: Text("¥100",
                            style: TextStyle(color: Colors.red)),
                    trailing: Text("x2"),
                  )
                ],
              ),
            ))
      ],
    ),
    

    3、订单信息

    ListTile(
     title: Row(
       children: <Widget>[
         Text("订单编号:",style: TextStyle(fontWeight: FontWeight.bold)),
         Text("124215215xx324")
       ],
     ),
    ),
    
    ListTile(
     title: Row(
       children: <Widget>[
         Text("下单日期:",style: TextStyle(fontWeight: FontWeight.bold)),
         Text("2019-12-09")
       ],
     ),
    ),
    
    ListTile(
     title: Row(
       children: <Widget>[
         Text("支付方式:",style: TextStyle(fontWeight: FontWeight.bold)),
         Text("微信支付")
       ],
     ),
    ),
    
    ListTile(
     title: Row(
       children: <Widget>[
         Text("配送方式:",style: TextStyle(fontWeight: FontWeight.bold)),
         Text("顺丰")
       ],
     ),
    )
    
    children: <Widget>[
      Text("总金额:",style: TextStyle(fontWeight: FontWeight.bold)),
      Text("¥414元",style: TextStyle(
        color: Colors.red
      ))
    ],
    

    Demo

    Demo在我的Github上,欢迎下载。
    JDShop_Flutter

    推荐的Flutter项目,可以模仿学习
    豆瓣 APP

    学习资料

    相关文章

      网友评论

        本文标题:Flutter:仿京东商城APP的完整开发指南(三)

        本文链接:https://www.haomeiwen.com/subject/zgglgktx.html