【React Native 实战:构建电商 App】12-实现购买之下单

React Native 实战:构建电商 App

知识点:

  • 地址列表
  • 地址编辑添加
  • 下单结果页

地址列表页

下单需要选择地址,这里我们再做一个地址列表页,点击选择地址的时候跳转到这个页面,选择完成之后回调选择好的地址结果。

新建文件 /src/center/addressList.js,在文件中创建一个简单的列表,这里把列表和内容分开,使用几个空的方法替代。

{/*地址列表*/}
            <FlatList
                data={this.state.list}
                refreshing={this.state.refreshing}
                onRefresh={() => this.refresh()}
                keyExtractor={(item) => item.id}
                renderItem={({ item }) =>
                    <AddressItem
                        address={item}
                        select={this.select.bind(this)}
                        defaultSelect={this.defaultSelect.bind(this)}
                        goEdit={this.goEdit.bind(this)}
                        delAddress={this.delAddress.bind(this)} />
                }
                onEndReached={() => this.next()}
                ListEmptyComponent={
                    <Text style={styles.emptyList} allowFontScaling={false}>暂无收货地址</Text>
                }
            />

每一项地址都包含选择、编辑、删除、设为默认 4 个事件,这里使用列表传入的事件,本身并不处理事件。

const { address, selected, isSelect, isSelected } = this.props
        return <View style={styles.addressItem} key={address.id}>
            <TouchableWithoutFeedback onPress={() => this.props.select(address)}>
                <View style={styles.addressInfo}>
                    {isSelect &&
                        <View style={styles.addressCheck}>
                            {isSelected &&
                                <Image source={{ uri: require('../images/icon-address-check') }}
                                    style={{ width: px(30), height: px(20), marginTop: px(50) }} />
                            }
                        </View>
                    }
                    <View style={styles.addressDetail}>
                        <View style={styles.head}>
                            <Text allowFontScaling={false} style={[styles.addressDetail1, {
                                lineHeight: px(35)
                            }]}>{address.name}  {address.phone}</Text>
                            {address.defaultYn == 'Y' && <View style={styles.normalBtn}>
                                <Text allowFontScaling={false} style={styles.normalText}>默认地址</Text>
                            </View>}
                        </View>
                        <View>
                            <Text allowFontScaling={false} style={styles.addressDetail2}>
                                {address.province}-{address.city}-{address.district}
                                {address.detail}
                            </Text>
                        </View>
                        <View>
                            <Text allowFontScaling={false} style={styles.addressDetail2}>{address.cardNo}</Text>
                        </View>
                    </View>
                </View>
            </TouchableWithoutFeedback>
            <View style={styles.addressAction}>
                <TouchableWithoutFeedback onPress={() => this.props.defaultSelect(address.id)}>
                    <View style={styles.radio}>
                        {
                            address.defaultYn == 'Y' && <Image source={{ uri: require('../images/icon-default-address') }}
                                resizeMode='cover'
                                style={{ width: px(34), height: px(34) }} />
                        }
                        {
                            address.defaultYn == 'N' && <Image source={{ uri: require('../images/icon-default-address-un') }}
                                resizeMode='cover'
                                style={{ width: px(34), height: px(34) }} />
                        }
                        <Text allowFontScaling={false} style={styles.radioLabel}>
                            {address.defaultYn == 'Y' ? '默认地址' : '设为默认'}
                        </Text>
                    </View>
                </TouchableWithoutFeedback>
                <TouchableWithoutFeedback onPress={() => this.props.goEdit(address.id)}>
                    <View style={styles.addressActionBtn}>
                        <Image style={{ width: px(24), height: px(24), marginTop: px(1) }} source={{ uri: require('../images/icon-address-edit') }}></Image>
                        <Text allowFontScaling={false} style={styles.addressActionBtnTxt}>编辑</Text>
                    </View>
                </TouchableWithoutFeedback>
                <TouchableWithoutFeedback onPress={() => this.props.delAddress(address.id)}>
                    <View style={styles.addressActionBtn}>
                        <Image style={{ width: px(25), height: px(25) }} source={{ uri: require('../images/icon-address-del') }}></Image>
                        <Text allowFontScaling={false} style={styles.addressActionBtnTxt}>删除</Text>
                    </View>
                </TouchableWithoutFeedback>
            </View>
        </View>

给列表添加几个事件,这里仅作参考,具体的逻辑看接口返回的数据格式而定。

componentDidMount() {
        this.refresh();
    }
    //获取第一页
    refresh() {
        let list = [{
            id: "2",
            name: "收货人",
            phone: "15600222222",
            province: "北京",
            city: "北京",
            district: "朝阳区",
            detail: "朝阳大悦城xxx号xxx店"
        }]
        this.setState({ list })
        //真是情况的代码
        // try {
        //     this.start = 1;
        //     let list = request.get('/get/address,do', {
        //         start: this.start
        //     })
        //     if (!list || list.constructor !== Array) return;
        //     this.setState({ list })
        // } catch (e) {
        //     toast(e.message)
        // }
    }

选中地址的时候使用回调方法,将地址放入上一页的地址栏中。同时调用路由的返回方法。

//选中并返回
select(address) {
     if (this.state.callback) this.state.callback(address);
     this.props.navigation.goBack();
 }

远程请求做一个例子,这里先注释掉假装已经能用了。

//设置默认地址
defaultSelect(id) { 
    // try {
    //     request.post("/set/address.do",{id:id})
    //     this.refresh();
    // } catch (e) {
    //     toast(e.message)
    // }
}
//删除
delAddress(id) { 
    // try {
    //     request.post("/del/address.do",{id:id})
    //     this.refresh();
    // } catch (e) {
    //     toast(e.message)
    // }
}

这里把新建和修改放在一个页面,通过 ID 的是否存在来判断使用哪个模式。

//跳转编辑
goEdit(id) {
     this.props.navigation.navigate('AddressEdit', {
         id,
         callback: () => this.refresh()
     });
 }
 //添加新地址
 goCreate() {
     this.props.navigation.navigate('AddressEdit', {
         callback: () => this.refresh()
     });
 }

地址编辑页

地址编辑页其实就是一个表单提交页,各家的方法各不相同,这里我介绍一种我在使用的方式。

进入页面的时候会检查本地是否有地址数据包,然后将本地数据包的时间戳上报给服务器,服务器判断数据是否需要更新,然后返回新数据包或者空。下面再呈现正常的界面,这里的数据包其实就是省市三级联动的具体城市内容,我在这里做一个简单的模拟。

这里先看一下样式:

enter image description here

再看一下代码的结构,这里就不显示全部的代码了。

enter image description here

在组件初始化的时候去拿数据,通过时间戳来判断是否有更新,将数据放在变量中。这里可以适当放一个 loading 动画。

address_data = {}
async componentDidMount() {
    try {
        //获取默认数据
        let data = await getItem("address")
        if (!data) data = { time: 0 }
        //使用时间戳获取数据
        let res = await request.get("get/address/data", { time: data.time })
        //有返回则更新本地数据
        if (res) {
            data = res;
            setItem("address", data);
        }
        //复制给组件
        this.address_data = data;
    } catch (e) {
        toast(e.message)
    }
}

最后在保存的时候将地址回传给上一页,到这里地址的编辑和选择就结束了。

//保存地址
async save() {
    let data={
        id:this.state.id,
    }
    if (!this.state.name) {
        toast('请输入收货人姓名'); return;
    }
    if (!this.state.phone) {
        toast('请输入收货人电话'); return;
    }
    if (!this.state.detail) {
        toast('请输入收货地址'); return;
    }
    // try {
    //     let data=request.post("/address",this.state);
    // } catch (e) {
    //     toast(e.message)
    // }
    this.callback && this.callback(this.state);
    this.props.navigation.goBack();
}

下单结果页

通常在下单之后都要进入一个支付结果页,这个页面一方面是通知用户支付的结果,同时也是再一次查询服务器,对比支付的真实情况。

在微信支付之后增加一次跳转,不管支付成功与否都跳转。

finally {
   this.props.navigation.navigate('Success', { orderNo: '' })
}

按照下面的样子做一个类似的界面,这里要判断支付结果,显示隐藏支付成功和失败的样子。

enter image description here

enter image description here

照例使用假数据调试这个页面:

async componentDidMount() {
  this.setState({
      image: "http://imgbeta.daling.com/data/files/mobile/2016/06/08/14653741419171.jpg_710x440.jpg",
      orderNo: "NS2029284774849",
      payAmount: "200.00",
      pay: "1"
  })
  // try {
  //     let data = request.get("/get/order", { orderid: this.state.orderNo })
  // } catch (e) {
  //     toast(e.message)
  // }
}

这里还要记得处理一下安卓的物理返回键,不然用户点击返回就回到了上一页的下单页了。

 //添加物理返回键的监听
 componentWillMount() {
     BackHandler.addEventListener('hardwareBackPress', this.noGoBack);
 }
 //注销监听
 componentWillUnmount() {
     BackHandler.removeEventListener("hardwareBackPress", this.noGoBack);
 }
 //禁止返回
 noGoBack() {
     return true;
 }

其他跳转还是按照正常的来,但是回到首页需要重置路由。当前的路由栈里已经存在了下单以及详情等页面,这个时候直接跳转会让之前的路由继续存在,但是我们又不能让用户返回到上一页,此时就会出现 bug,所有这里直接重置路由的内容,然后路由变成正常的样子。

//跳转到订单详情页
goDetail() {
    this.props.navigation.navigate('OrderDetail', {
        orderNo: this.state.orderNo
    });
}
//重置路由到首页
goHome() {
    this.props.navigation.dispatch(NavigationActions.reset({
        index: 0,
        actions: [
            NavigationActions.navigate({ routeName: 'Tabs' })
        ]
    }));
}
//调用支付
async pay() {
    let isInstalled = await isWXAppInstalled();
    if (!isInstalled) {
        toast('请安装微信客户端');
        return;
    }
    try {
        let res = await pay(wx_data);
        //微信返回结果
    } catch (e) {
        //微信调起失败/取消
    }
}

这里有一个小窍门,在页面转换的时候可以将页面的信息记录在一个单独的地方,这样就可以随时查询路由栈中的顺序了。

如果遇到请求不到线上产品的,请检查网络、代理设置。如果还有问题,请修改 /src/utils/request.js,使用模拟的 header 信息。

let headers = {
    uid: 0,
    jsversion: "0.1.1",
    utoken: "",
    platform: Platform.OS,
    clientid: "db7b4e395b7a0ec75c6078ddf7db276c588a694f",
    version: '1.0.5',
    model: "apple",
    OSVersion: '11.2',
    brand: "apple",
    channel: "appstore",
    net: 'WIFI',
    bundle: "13b2bcaba812600af5465c8c649f34d3",
    xcrole: 0, 
}

发表评论