vue 自定义v-model 封装地址选择组件,并实现数据绑定和表单验证

vue 自定义v-model 封装地址选择组件,并实现数据绑定和表单验证

vue是双向数据绑定的,v-model可以自动搜集数据,这在我们使用过程中可以说是非常方便。但是,在开发中,如果想把代码写的更精简,提供更多的复用。那么我们就免不了想自己封装一个用有v-model属性的组件。(本人工作中,就迫切有这种需求,因为表单页面太大,如果不做封装精简,就算用了element-ui这种已经封装过的框架,页面依然会很庞大!)

关于v-model

要实现自己的v-model,首先要了解到,v-model实际上是由两部分组成的,即value和input事件,例如下面两行代码,是等价的

<input v-model="name">
<input :value="name" @input="name=$event.target.value">

知道了原理,我们就可以开干了。
下面以分装一个三联动地址选择的小组件为例,使用的select基于element-ui
新建vue组件choose-address-form-item.vue
这里封装一个表单中的地址选择组件,所以默认认为他的父组件由<el-form>标签

封装组件

先上html部分代码,代码使用flex布局,样式相关的类名可忽略。这里说明下 rowStart样式:

.rowStart {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}

<el-row>
        <el-form-item :label="title" label-position="top" class="addressFormItemBox" :required="required" :prop="addressProp">
            <div v-if="edit" class="rowStart">
                <!--@change="changeProvince"-->
                <el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince"  placeholder="请选择">
                    <el-option
                            v-for="item in provinceList"
                            :key="item.regionCode"
                            :label="item.regionName"
                            :value="item.regionCode">
                    </el-option>
                </el-select>
                <el-select class="addressFormItem" :size="size" :value="address.cityId" @change="changeCity" placeholder="请选择">
                    <el-option
                            v-for="item in cityList"
                            :key="item.regionCode"
                            :label="item.regionName"
                            :value="item.regionCode">
                    </el-option>
                </el-select>
                <el-select class="addressFormItem" v-if="isNotTwoLevels" :size="size" :value="address.districtId" @change="changeDistrict" placeholder="请选择">
                    <el-option
                            v-for="item in districtList"
                            :key="item.regionCode"
                            :label="item.regionName"
                            :value="item.regionCode">
                    </el-option>
                </el-select>
                <el-input class="addressFormInput" v-if="showStreet" :style="{width:streetInputWidth}" :size="size" placeholder="请输入" :value="address.street" @blur="streetBlur" @input="streetInput"></el-input>
            </div>
            <span v-else>{{fullAddress}}</span>
        </el-form-item>
    </el-row>

这里重点说下el-select和el-input的拆分。el-select本来的v-model被重新写为 :value="address.provinceId" @input="changeProvince"

  <el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince"  placeholder="请选择">
                    <el-option
                            v-for="item in provinceList"
                            :key="item.regionCode"
                            :label="item.regionName"
                            :value="item.regionCode">
                    </el-option>
                </el-select>

由于地址里面有三个input标签,为了方便处理,并且能实现表单验证,所以统一用address来接收数据
address.provinceId是省,address.cityId是城市,address.districtId是县
el-form-item上,:prop="addressProp",指定prop用于设置prop属性,便于表单验证。
isNotTwoLevels 用于判断是否是直辖市,直辖市只有两级,隐藏县级。

下面看看js部分

<script>
    import {mapState} from 'vuex'
    import {reqRegionInfo} from "../api/commonApi"
    import {isNumber} from "../utils/validate";

    export default {
        name: 'addressFormItem',
        props:{
            required:{
                type:Boolean,
                default:true
            },//是否必须
            title:{
                type:String,
                default:'选择地址'
            },//标题
            showStreet:{
                type:Boolean,
                default:true
            },//是否显示输入详情地址
            defaultAddress:{
                type:Object,
                default:()=>{
                    return {}
                }
            },//默认地址
            edit:{
                type:Boolean,
                default:true
            },//是否可编辑
            size:{
                type:String,
                default:'mini'
            },//尺寸
            inputWidth:{
                type:[Number,String],
                default:'auto'
            },
            addressProp:{
                type:String,
                default:'address'
            }
        },
        data() {
            return {
                address:{},//地址
                isNotTwoLevels:false,//是否直辖市
                cityList:[],//市
                districtList:[],//县
                fullAddress:'',//地址全部信息
                projectAddress:[],//地址数组数据
                streetInputWidth:'auto',//地址输入框宽度
                getDefault:false,//是否获取了默认值
            }
        },

        computed: {
            ...mapState(['provinceList'])
        },
        async mounted() {
            let {inputWidth}=this
            this.streetInputWidth=(typeof inputWidth==='number' || isNumber(inputWidth)) ? (inputWidth+'px') : inputWidth
            this.address=this.defaultAddress
        },
        methods: {
            // 获取省市区信息 code 父级code 000000 省份  type类型 (province省份 city城市 district区域县  )
            async getRegionInfo(code,type=0){
                let result=await reqRegionInfo(code)
                // //console.log(result)
                this[['provinceList','cityList','districtList'][type]]=result
                ;(type===2) && (this.isNotTwoLevels=!!result.length)
                // //console.log(this.isNotTwoLevels)
            },
            // 点击切换省份
            changeProvince(val){
                this.$set(this.address,'provinceId',val)
                this.$set(this.address,'cityId','')
                this.$set(this.address,'districtId','')
                this.$set(this.address,'street','')
                this.districtList  = []
                this.cityList  = []
                this.getRegionInfo(val,1)
                let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
                // //console.log(thisProvince)
                this.projectAddress[1]=this.projectAddress[2]=''
                this.projectAddress[0]=[thisProvince[0].regionName]
                this.address.projectAddress= this.projectAddress
                this.$emit('input',this.address)
            },
            // 点击切换城市
            changeCity(val){
                this.districtList  = []
                this.getRegionInfo(val,2)
                this.$set(this.address,'cityId',val)
                this.$set(this.address,'districtId','')
                // //console.log(this.cityList)
                let thisCity=this.cityList.filter(item=>item.regionCode===val)
                this.projectAddress[2]=''
                this.projectAddress[1]=thisCity[0].regionName
                this.address.projectAddress= this.projectAddress
                this.$emit('input',this.address)
            },
            // 点击切换区县
            async changeDistrict(val){
                let {districtList}=this
                this.$set(this.address,'districtId',val)
                // //console.log(this.address)
                let thisdistrictList=districtList.filter(item=>item.regionCode===val)
                this.projectAddress[2]=thisdistrictList[0].regionName
                this.projectAddress[3] && this.getLonLat(this.projectAddress.join(''))
                this.address.projectAddress= this.projectAddress
                this.address.showStreet= this.showStreet
                this.$emit('input',this.address)

            },
            //获取经纬度
            async getLonLat(data){
                let lngLatArr = await this.$globalMethods.getLngLat(AMap,data)
                // //console.log(lngLatArr)
                let {projectAddress,isNotTwoLevels,showStreet}=this
                this.address={...this.address,projectAddress,isNotTwoLevels,showStreet}
                this.$emit('getLngLatInfo',{
                    longitude:lngLatArr[0].lng,
                    latitude:lngLatArr[0].lat,
                })
                // //console.log(this.address)
                this.$emit('input',this.address)
            },
            //详细地址改变
            streetBlur(e){
                this.projectAddress[3]=e.target.value
                // //console.log(this.projectAddress.join(''))
                ;((this.isNotTwoLevels && this.projectAddress[1]) || this.projectAddress[2]) && this.getLonLat(this.projectAddress.join(''))
                this.address.projectAddress= this.projectAddress
                let {isNotTwoLevels,showStreet}=this
                this.address={...this.address,projectAddress:this.projectAddress,isNotTwoLevels,showStreet}
                this.$emit('input',this.address)
            },
            //
            streetInput(value){
                this.$set(this.address,'street',value)
                this.$emit('input', this.address)
            }
        },
        watch:{
            defaultAddress:{
                deep:true,
                handler:async function (value) {
                    console.log(value)
                    let {cityId,provinceId,districtId ,street }=value
                    if(this.getDefault) return
                    if(provinceId && cityId){
                        let cityList = await reqRegionInfo(provinceId)
                        this.cityList=cityList
                        let districtList = await reqRegionInfo(cityId)
                        this.districtList=districtList
                        //console.log(districtList)
                        this.isNotTwoLevels=!!districtList.length
                        this.address={...value}
                        this.getDefault=true
                        let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
                        let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
                        let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
                        this.fullAddress=`${province} ${city} ${district} ${street}`
                    }
                }
            },
        }

    }
</script>
首先说下props部分

重点的:

 defaultAddress:{
                type:Object,
                default:()=>{
                    return {}
                }
            },//默认地址

考虑到编辑状态,会从后台获取数据显示默认数据,用defaultAddress接收,
对应的需要在watch里面做监听。并把值赋给address

 watch:{
            defaultAddress:{
                deep:true,
                handler:async function (value) {
                    console.log(value)
                    if(!value){
                        //没有数据时,清空
                        this.address={}
                        this.cityList=[]
                        this.districtList=[]
                    }
                    let {cityId,provinceId,districtId ,street }=value
                    //有数据时只允许更新一次
                    if(this.getDefault) return
                    if(provinceId && cityId){
                        let cityList = await reqRegionInfo(provinceId)
                        this.cityList=cityList
                        let districtList = await reqRegionInfo(cityId)
                        this.districtList=districtList
                        //console.log(districtList)
                        this.isNotTwoLevels=!!districtList.length
                        this.address={...value}
                        this.getDefault=true
                        let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
                        let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
                        let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
                        this.fullAddress=`${province} ${city} ${district} ${street}`
                    }
                }
            },
        }
methods部分

看重点:

 // 点击切换省份
            changeProvince(val){
                this.$set(this.address,'provinceId',val)
                this.$set(this.address,'cityId','')
                this.$set(this.address,'districtId','')
                this.$set(this.address,'street','')
                this.districtList  = []
                this.cityList  = []
                this.getRegionInfo(val,1)
                let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
                // //console.log(thisProvince)
                this.projectAddress[1]=this.projectAddress[2]=''
                this.projectAddress[0]=[thisProvince[0].regionName]
                this.address.projectAddress= this.projectAddress
                this.$emit('input',this.address)
            },
            // 点击切换城市
            changeCity(val){
                this.districtList  = []
                this.getRegionInfo(val,2)
                this.$set(this.address,'cityId',val)
                this.$set(this.address,'districtId','')
                // //console.log(this.cityList)
                let thisCity=this.cityList.filter(item=>item.regionCode===val)
                this.projectAddress[2]=''
                this.projectAddress[1]=thisCity[0].regionName
                this.address.projectAddress= this.projectAddress
                this.$emit('input',this.address)
            },
            //详细地址输入
             streetInput(value){
                this.$set(this.address,'street',value)
                this.$emit('input', this.address)
            }

这里的重点在于,当下拉框发生改变,输入框发生改变时,要及时把数据返给父级组件:
在changeProvince函数中,changeCity函数中,streetInput中,均需要执行 this.$emit('input',this.address)

组件使用和表单验证

封装完了,开始使用
在views中新建form.vue,并且引用ChooseAddressFormItem组件:

 import ChooseAddressFormItem from ../components/ChooseAddressFormItem.vue

在form.vue template中使用:

  <ChooseAddressFormItem title="项目地址:" @getLngLatInfo="getLngLatInfo" size="larger" input-width="400px" v-model="projCardForm.address" addressProp=“address"/>

这里重点有三:
第一个是v-model="projCardForm.address",这里是数据绑定;
第二是addressProp=“address,指定子组件prop属性,用于表单验证,
第三,表单验证:下面仔细说下表单验证
由于要验证的是一个对象,并且还有存在直辖市等特殊情况,不能依靠element-ui本身的基础验证,需要自定义,在表单验证数据rules中

rules: {
              address:[{validator:(rule, value, callback)=>validAddressInfo(rule, value, callback),trigger:['blur', 'change']}],
          },

element-ui提供了自定义验证方式validator函数,参数有rule,value,callback,这里单独去定义一个验证函数
src下面新建utils文件夹,utils文件夹下面新建validateMethods.js
在validateMethods.js里面定义地址验证方法
validateMethods.js

//检查地址是否完善——地址封装组件
export const validAddressInfo=(rule, value, callback,msg='请完善地址信息')=>{
//如果值不是对象,肯定不通过,调用  callback(new Error(msg))函数
   if(!value || !(value instanceof Object)){
       callback(new Error(msg))
       return
   }
   let {districtId,isNotTwoLevels,showStreet,street}=value
   //显示地址输入框的时候,如果地址输入框没有值,肯定不通过
   if(showStreet){
       if(!street){
           callback(new Error(msg))
           return
       }
   }
   最后的情况,非直辖市情况下,没有县id,肯定不通过
   if(!districtId && isNotTwoLevels){
       callback(new Error(msg))
   }
}

那么现在在form.vue中引入地址验证函数validAddressInfo,然后赋值给rule中的validator就行

<script>
  import {validAddressInfo} from '../utils/validateMethods.js'
export default{
data(){
return{
rules: {
             address:[{validator:(rule, value, callback)=>validAddressInfo(rule, value, callback),trigger:['blur', 'change']}],
         },
    }
}
}
</script>

到此,组件封装和使用讲完。不仅简化了代码,而且数据绑定,表单验证都没少。
后面附上组件全部代码
由于本组件地址联动数据通过服务器请求获取的,请自动忽略,你只需要找到相关数据对上即可

<!--选择地址-->
<template>
  <el-row>
      <el-form-item :label="title" label-position="top" class="addressFormItemBox" :required="required" :prop="addressProp">
          <div v-if="edit" class="rowStart">
              <!--@change="changeProvince"-->
              <el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince"  placeholder="请选择">
                  <el-option
                          v-for="item in provinceList"
                          :key="item.regionCode"
                          :label="item.regionName"
                          :value="item.regionCode">
                  </el-option>
              </el-select>
              <el-select class="addressFormItem" :size="size" :value="address.cityId" @change="changeCity" placeholder="请选择">
                  <el-option
                          v-for="item in cityList"
                          :key="item.regionCode"
                          :label="item.regionName"
                          :value="item.regionCode">
                  </el-option>
              </el-select>
              <el-select class="addressFormItem" v-if="isNotTwoLevels" :size="size" :value="address.districtId" @change="changeDistrict" placeholder="请选择">
                  <el-option
                          v-for="item in districtList"
                          :key="item.regionCode"
                          :label="item.regionName"
                          :value="item.regionCode">
                  </el-option>
              </el-select>
              <el-input class="addressFormInput" v-if="showStreet" :style="{width:streetInputWidth}" :size="size" placeholder="请输入" :value="address.street" @blur="streetBlur" @input="streetInput"></el-input>
          </div>
          <span v-else>{{fullAddress}}</span>
      </el-form-item>
  </el-row>

</template>

<script>
  import {mapState} from 'vuex'
  import {reqRegionInfo} from "../api/commonApi"
  import {isNumber} from "../utils/validate";

  export default {
      name: 'addressFormItem',
      props:{
          required:{
              type:Boolean,
              default:true
          },//是否必须
          title:{
              type:String,
              default:'选择地址'
          },//标题
          showStreet:{
              type:Boolean,
              default:true
          },//是否显示输入详情地址
          defaultAddress:{
              type:Object,
              default:()=>{
                  return {}
              }
          },//默认地址
          edit:{
              type:Boolean,
              default:true
          },//是否可编辑
          size:{
              type:String,
              default:'mini'
          },//尺寸
          inputWidth:{
              type:[Number,String],
              default:'auto'
          },
          addressProp:{
              type:String,
              default:'address'
          }
      },
      data() {
          return {
              address:{},//地址
              isNotTwoLevels:false,//是否直辖市
              cityList:[],//市
              districtList:[],//县
              fullAddress:'',//地址全部信息
              projectAddress:[],//地址数组数据
              streetInputWidth:'auto',//地址输入框宽度
              getDefault:false,//是否获取了默认值
          }
      },

      computed: {
          ...mapState(['provinceList'])
      },
      async mounted() {
          let {inputWidth}=this
          this.streetInputWidth=(typeof inputWidth==='number' || isNumber(inputWidth)) ? (inputWidth+'px') : inputWidth
          this.address=this.defaultAddress
      },
      methods: {
          // 获取省市区信息 code 父级code 000000 省份  type类型 (province省份 city城市 district区域县  )
          async getRegionInfo(code,type=0){
              let result=await reqRegionInfo(code)
              // //console.log(result)
              this[['provinceList','cityList','districtList'][type]]=result
              ;(type===2) && (this.isNotTwoLevels=!!result.length)
              // //console.log(this.isNotTwoLevels)
          },
          // 点击切换省份
          changeProvince(val){
              this.$set(this.address,'provinceId',val)
              this.$set(this.address,'cityId','')
              this.$set(this.address,'districtId','')
              this.$set(this.address,'street','')
              this.districtList  = []
              this.cityList  = []
              this.getRegionInfo(val,1)
              let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
              // //console.log(thisProvince)
              this.projectAddress[1]=this.projectAddress[2]=''
              this.projectAddress[0]=[thisProvince[0].regionName]
              this.address.projectAddress= this.projectAddress
              this.$emit('input',this.address)
          },
          // 点击切换城市
          changeCity(val){
              this.districtList  = []
              this.getRegionInfo(val,2)
              this.$set(this.address,'cityId',val)
              this.$set(this.address,'districtId','')
              // //console.log(this.cityList)
              let thisCity=this.cityList.filter(item=>item.regionCode===val)
              this.projectAddress[2]=''
              this.projectAddress[1]=thisCity[0].regionName
              this.address.projectAddress= this.projectAddress
              this.$emit('input',this.address)
          },
          // 点击切换区县
          async changeDistrict(val){
              let {districtList}=this
              this.$set(this.address,'districtId',val)
              // //console.log(this.address)
              let thisdistrictList=districtList.filter(item=>item.regionCode===val)
              this.projectAddress[2]=thisdistrictList[0].regionName
              this.projectAddress[3] && this.getLonLat(this.projectAddress.join(''))
              this.address.projectAddress= this.projectAddress
              this.address.showStreet= this.showStreet
              this.$emit('input',this.address)

          },
          //获取经纬度
          async getLonLat(data){
              let lngLatArr = await this.$globalMethods.getLngLat(AMap,data)
              // //console.log(lngLatArr)
              let {projectAddress,isNotTwoLevels,showStreet}=this
              this.address={...this.address,projectAddress,isNotTwoLevels,showStreet}
              this.$emit('getLngLatInfo',{
                  longitude:lngLatArr[0].lng,
                  latitude:lngLatArr[0].lat,
              })
              // //console.log(this.address)
              this.$emit('input',this.address)
          },
          //详细地址改变
          streetBlur(e){
              this.projectAddress[3]=e.target.value
              // //console.log(this.projectAddress.join(''))
              ;((this.isNotTwoLevels && this.projectAddress[1]) || this.projectAddress[2]) && this.getLonLat(this.projectAddress.join(''))
              this.address.projectAddress= this.projectAddress
              let {isNotTwoLevels,showStreet}=this
              this.address={...this.address,projectAddress:this.projectAddress,isNotTwoLevels,showStreet}
              this.$emit('input',this.address)
          },
          //
          streetInput(value){
              this.$set(this.address,'street',value)
              this.$emit('input', this.address)
          }
      },
      watch:{
          defaultAddress:{
              deep:true,
              handler:async function (value) {
                  console.log(value)
                  if(!value){
                      //没有数据时,清空
                      this.address={}
                      this.cityList=[]
                      this.districtList=[]
                  }
                  let {cityId,provinceId,districtId ,street }=value
                  //有数据时只允许更新一次
                  if(this.getDefault) return
                  if(provinceId && cityId){
                      let cityList = await reqRegionInfo(provinceId)
                      this.cityList=cityList
                      let districtList = await reqRegionInfo(cityId)
                      this.districtList=districtList
                      //console.log(districtList)
                      this.isNotTwoLevels=!!districtList.length
                      this.address={...value}
                      this.getDefault=true
                      let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
                      let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
                      let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
                      this.fullAddress=`${province} ${city} ${district} ${street}`
                  }
              }
          },
      }

  }
</script>

<style scoped lang="scss">
  .addressFormItemBox{
      .addressFormItem{
          margin-right:10px;
      }
      .addressFormInput{
          /*flex:1;*/
      }
  }
</style>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342