vue的多页后台管理系统搭建

前言

最近我在公司用的前端技术组合是layui+knockout.js的组合。为什么呢?因为这是原来公司前端留下来的,以我的前段底子还算摸得清楚,用起来也不算复杂,于是就用下去了。但是吧,越用越难用。为什么呢?之所以使用这个组合,就是希望使用knockout的双向绑定功能取代dom操作,加上layui这样一个相对比较全的ui库,组合起来看似完美。但是吧,有几个组件,比如下拉选择,单选多选等等,就是需要你对其添加单独的绑定事件来处理数据,很多时候就是需要你手动刷新ui绘制才能有效果,这样搞起来就很烦。尤其是现在,越来越多的细致的功能需要去做,而重用的内容却很难搞,就陷入了泥沼之中。
再说说为啥我没有强改成vue。其实吧,我是研究过vue的,也练过手写过几篇博客在这个平台的有https://www.jianshu.com/p/29625af02d79。但是,一直以来我都没有搭建成功过一个多页应用,或者说可以自动扫描多页的应用。现在居然让我碰到了一个模板,地址是:https://github.com/Plortinus/vue-multiple-pages。然后只需要npm install就可以了。接下来我们从项目结构的分析开始吧。

基本情况

运行

这个代码down下来是可以直接运行的,下面是一些基本的操作:

npm run serve # 运行server进行调试
npm run build # 构建项目,会生成dist目录

配置

这里介绍一下主要的配置文件。

/vue.config.js

这个应该是vue项目的主要配置文件,这里可以看到对多页目录的 读取,server的基本配置。

/server.js

这个应该是运行的服务的配置,可以看到其读取的文件目录,貌似使用的是一个叫做express的包,具体我也还没有研究怎么用,后面再说吧。

/title.js

这里配置各个页面的标题,它在vue.config.js中有被使用到。或许我们可以在页面内部解决标题的问题,这个后面研究了再说。

目录结构

源代码的顶级目录分为public、src,其中public的内容很简单,看页面似乎是在js不运行的情况下展示的报错页面。src里面则是我们自己写的各种内容的页面。

/src/assets

静态资源目录

/src/components

组件目录,如果我们希望某个组件可以在不同的页面之间进行重用,把它放在这里。

/src/states

状态管理逻辑,所有的状态管理放在这里会有助于管理整个web端的状态,帮助理清思路。因为,状态,理论上来说是可以跨页面、跨组件的,之和业务本身的生命周期有关。

/src/pages

这里就是各个页面的代码了,要注意的是,页面是按照目录结构组织的,每个目录下面的app.js是该页面的入口,这个可以在vue.config.js文件中找到对应的使用。而每个页面是一个一个文件夹。访问页面的时候url为:http://域名:端口/目录结构.html,也即最后一个app.js所在的目录名字加上.html就是它的url啦。

实战demo

实际写页面的时候,我发现个问题,就是它默认的element-ui的版本太低了,于是我改成了最新的,2.13.2,这样就和官网文档的描述一致了。具体的修改方法是,在package.json文件中,找到element-ui,把后面的版本号改了。然后,在命令行里执行,npm install,就可以了。后面我们开始写页面吧。

修改登陆密码页面

由于我这个项目是补充之前项目的页面,所以不会从登陆、主页这样的写,于是我挑了个最简单的页面用来练手。修改登陆密码页面。先粘贴页面代码吧:

<template>
<div>
    <el-form ref="form" :model="form" label-width="80px">
        <el-form-item label="原密码">
            <el-input v-model="form.oldPwd" show-password></el-input>
        </el-form-item>
        <el-form-item label="新密码">
            <el-input v-model="form.newPwd" show-password></el-input>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="onSubmit">修改登陆密码</el-button>
        </el-form-item>
    </el-form>
</div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        oldPwd: '',
        newPwd: ''
      }
    }
  },
  methods: {
    onSubmit() {
      console.log('submit!')
    }
  }
}
</script>

<style>
</style>

上面的代码,是只绘制了页面的样子的代码,可以看到script里面只有空的data及methods,具体的业务逻辑都没有实现。需要注意的是,在template里面使用到的data也好、methods也好,都需要至少进行声明,否则编译不过去,也就无法调试了。具体的需要注意的细节,也就只有el-input最后的show-password了。这是element-ui内置的一个属性,它帮助我们实现了一个密码框,右侧还有按钮可以控制密码框显示明文还是点号。需要注意的是,这个效果就受限制与element-ui的版本,最初的那个版本就无法正常使用。

修改登陆密码请求

这个页面非常简单,唯一需要的业务,也就是把修改密码的请求提交到后台。这里我们就不能用jquery了,我们将会使用一个叫做axios的框架。安装命令如下:

npm install axios

在script中进行引入,代码如下:

import axios from 'axios'

然后就可以开始进行ajax请求啦,样例代码如下:

      axios.post(
          'http://localhost:8080/api/sys/users/' + userId + '/modifyPassword',
          {
            userId: userId,
            oldPwd: 'admin123',
            newPwd: 'admin1234'
          },
          {
            headers: {
              'Content-Type': 'application/json',
              SessionToken:
                '123456'
            }
          }
        )
        .then(data => {
          console.log(data)
        })

可以看到我们这是进行的post请求。它的请求格式其实是:axios#post(url[, data[, config]]) ,也就是说,第一个参数是要请求的url,这个是必填的,然后是请求的数据。这个data就是你要请求的json字符串。我不确定axios的请求默认是json的,所以,我在后面的config里面加入了json的header,还加入了我用来做会话验证的SessionToken的头。最后在then里面是回调。
这里需要说明的是,这种调用方式肯定不是最终的调用方式,最终还需要进行生产需要的封装,以在写业务的时候可以更少得关注这些细节。但是,有了这个,可以说我们就可以完成这个页面了。剩下的只是,也只是如何使用vue及其生态了。

路径别名

在代码中import自己的js如果总是用相对路径,其实韩式蛮烦人的一件事。但是,这里面的根路径又很不好用,所以就需要引入路径别名。由于我的项目是使用vue-cli 4.0的,所以它的配置文件是vue.config.js,添加如下代码即可:

const path = require('path') //引入path模块
function resolve( dir){
  return path.join(__dirname, dir) //path.join(__dirname)设置绝对路径
}

module.exports = {
  ……
  chainWebpack: (config) => {
    config.plugins.delete('named-chunks')
    config.resolve.alias
      //set第一个参数:设置的别名,第二个参数:设置的路径
      .set('@', resolve('./'))
      .set('components', resolve('./src/components'))
      .set('assets', resolve('./src/assets'))
      .set('pages', resolve('./src/pages'))
      .set('tools', resolve('./src/tools'))
  },
……

结合自己的代码进行改造吧。在使用的时候,我们使用@,就意味着在使用./,依此类推,就有了一些目录的简短的缩写了,方便使用。

加入统一认证中心

认证中心原理

最近能够研究vue+element-ui的技术组合,也是源于设计完成了统一的认证中心才可以在一个web端统一使用两种不同的web技术来进行组合。关键点有以下几点:

  • 一个验证会话的页面:注意,这里是页面,它被放在认证中心,所有需要验证会话的页面都知道它在哪里。
  • 需要验证会话的页面中,创建一个不可见的iframe,里面用来访问验证回话的页面。通信方式使用PostMessage进行,这种通信是基于Html5标准,同时可以跨域。通信的目的是为了从验证回话页面同步会话信息到当前域下面。
  • 基本的流程控制:由于是通过页面进行跨域通信的,所以整个通信都需要在页面加载完成之后进行。待验证页面加载完成之后,才可以操作iframe的dom。iframe页面加载完成之后才可以向其发送验证请求。而验证请求的发送和接收都是异步的,页面并不会进行阻塞等待。如何处理这个流程也是该功能是否流畅稳定的关键。

统一模板组件

如何在每个页面中加入统一认证的过程,又不会在编写页面中引入过多的复杂度,这在vue的组件化体系中,我还是摸索了几种想法的,现在把我的心路历程整理下:

  • 每个页面加入一个验证组件:以前我在写vue的单页应用的例子的时候,登录页面和主页是在同一个页面中存在的,中间使用了一个登录状态标志位进行区分。整个效果是挺灵敏的,但是问题就是每个页面都会显示得直到这个逻辑的存在,而且需要在html的结构和js代码中显示得直到这个知识,这是很讨厌的一件事。但是,因为这个逻辑肯定可行,所以我后面的思路就变成了怎么把这个逻辑替换掉。
  • 页面路由:上述方案肯定能实现这个过程,但是关于验证过程的顺序要求就变得有些麻烦了。onload事件的顺序没有问题,但是PostMessage的异步通信要怎么解决。因为这个时候,页面本身可能也有需要在onload事件里执行的请求,而且需要用到其最终结果,会话标志。之前我思考到的方案是设置一个是否已验证的标志位,在未验证的时候,所有的网络请求会进入一个队列而不是直接请求。在收到验证结果后,依次执行这个队列里的请求。也是因为这个思路,我蛮不想用它的。而如果整个过程有一个页面跳转作为分隔,就变得简单多了。不过,在我找页面路由的实现方法时,我找到了我现在用的方案。
  • 写一个通用组件,里面加一个slot用来呈现页面。在vue中有一种机制叫做slot,他可以让我们改变组件里面的内容。毕竟组件的使用形式是标签,而如果通过标签里面的内容来改变组件的呈现,这似乎就是我想要的。而因为组件还可以有自己的代码逻辑,所以验证的过程就被有效得被隔离开了。于是,一切都变得那么自然,这个通用组件做我页面的顶级元素就可以了。

组件实现与应用

以下是template部分的代码:

<template>
<needAuthTemplate>
    <el-form ref="form" :model="form" label-width="80px">
        <el-form-item label="原密码">
            <el-input v-model="form.oldPwd" show-password></el-input>
        </el-form-item>
        <el-form-item label="新密码">
            <el-input v-model="form.newPwd" show-password></el-input>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="onSubmit">修改登陆密码</el-button>
        </el-form-item>
    </el-form>
</needAuthTemplate>
</template>

其中,needAuthTemplate是我定义的组件,它的代码如下:

<template>
<div>
  <slot></slot>
</div>
</template>
<script>
import config from '@/config.js'
console.log(config)

export default {
  data() {
    return {
      // configData: config.authUrl
      configData: 'http://localhost/page/sys/sessionCheck.html'
    }
  },
  methods: {
    sendCheckCmd(e) {
      console.log('sendCheckCmd:' + e)
    }
  },
  mounted: function() {
    /**
     * 注册监听事件
     */
    window.addEventListener(
      'message',
      function(e) {
        //如果e中含有type,说明是系统的消息,否则则尝试解析自定义消息
        if (e.data.type != undefined) {
          return
        }
        console.log(e.data)
        if (e == undefined || e == null) {
          return
        }

        var cmdData = JSON.parse(e.data) //获取json对象
        //如果cmdType不存在,说明入参非法
        if (cmdData.cmdType == undefined || cmdData == null) {
          return
        }

        switch (cmdData.cmdType) {
          case 'info':
            console.log(cmdData.sessionId)
            if (cmdData.sessionId == undefined || cmdData.sessionId == null) {
              //跳转登录
              window.location = 'http://localhost'
            } else {
              //刷新会话标志
              localStorage.setItem('sessionId', cmdData.sessionId)
              //加载后续页面
            }
            break
        }
      },
      false
    )
    //创建用来验证会话的iframe
    let bodyDoc = document.querySelector('body')
    let iframe = document.createElement('iframe')
    iframe.style.width = '0px'
    iframe.style.height = '0px'
    iframe.style.display = 'none'

    iframe.onload = () => {
      var cmdData = {}
      cmdData.cmdType = 'check'
      iframe.contentWindow.postMessage(JSON.stringify(cmdData), '*')
      console.log('加载完成') // 这样每次都会触发
    }
    iframe.src = this.configData

    //将dom附加到窗口中
    bodyDoc.appendChild(iframe)
    console.log('mounted')
  }
}
</script>
<style>
</style>

这个代码相对较长,有这么而几个点需要注意。

  • template部分,只是一个div嵌套了一个slot,也就是说这个组件展示的内容就是slot的内容。
  • script部分,可以被分为两个部分。第一段是给页面添加消息监听事件,用来处理接收会话消息;第二段则是在当前页面添加iframe的dom,在这个dom里我们访问会话验证页面。而且,注意添加dom的顺序,一定是一切都配置好了再添加到页面上。

但是这个时候其实还是有问题的:

  • vue的版本:这个问题是我在读slot的的文档的时候看到的,我默认的版本是2.5.x的版本,但是2.6.0以后,slot的标签语法发生了变化,所以,我升级到了最新的版本。需要注意的是vue和vue-template-compiler的版本要一起升级,否则编译不过。
  • 要让template中可以使用这个标签,是需要在组件文件夹额外写一个index.js文件的,文件内容为:
import needAuthTemplateComponent from './needAuthTemplate.vue'

const needAuthTemplate = {
  install: function(Vue) {
    Vue.component('NeedAuthTemplate', needAuthTemplateComponent)
  }
}

export default needAuthTemplate

注意,标签的名字是Vue.component的第一个参数。而在使用的页面的app.js的里面引入这个组件的代码也从直接引入vue文件变成了引入组件的目录,如下:

import NeedAuthTemplate from 'components/needAuthTemplate/'

Vue.use(NeedAuthTemplate)

认证中心坑

从实践来看,这次的实现依然完美实现多系统的统一认证,原因有两点:

  1. 每次都加载页面进行验证,其效率并不高,整体性能还是比较慢的。
  2. 这种设计其实并没有解决之前我说的因为异步需要等待的问题。也就是,如果你切换用户,原有页面你要刷新两次才能看到新用户的信息。而如果进行阻塞性的等待,则每次加载页面用户都会有显示的等待时间。

基于上述原因,我还是在登录的地方直接做了多域登录,现在的方式就只作为会话同步的手段了。

vue填坑记

上面我们看到了搭建认证中心过程中的坑。搭建完认证中心后,由于我们已经存在了主框架,所以接下来的重点是编写每个页面。而编写过程中遇到的重要内容,我将会记录在此。

methods中的search方法

记住,方法名千万不能用search,这似乎是methods中的内置方法名。我在用了这个方法名之后,一旦在mounted中调用,就会导致编译报错。该错误,我调试了一天,以此谨记。

通过props与子组件进行通信

这次,我的场景是一个非常常见的场景,添加和修改的弹窗。这两个弹窗我做成了一个组件。组件只是弹窗里面的内容,并不包含弹窗,方便以后复用。我需要将记录的ID传入组件内,以方便根据是否存在需要加载的ID来判断当前是当前是新增还是编辑。
所以,我需要解决这样几个问题:

  • 将ID从父组件传入进来
  • 持续得监听ID的变化

将ID从父组件传入进来

这件事情,说来简单,但是由于官方文档在介绍代码的时候并不是以vue文件的方式来介绍的,而且在说的时候不知道代码是在父组件还是子组件,所以让人困惑的老想做些别的事情。现整理如下:
首先是子组件的script部分,因为和其它部分没有关系,所以就不展示了。

export default {
  name: 'SaveLampSpecification',
  props: ['lampSpecificationId'],
  data() {
    let self = this
    return {
      form: {
        //灯具规格ID
        lampSpecificationId: self.lampSpecificationId,
        ……
      }
    }
  }
}

父组件的使用代码如下:

<template>
……
  <el-dialog title="添加灯具规格" :visible.sync="control.saveFormVisible" width="500px">
    <SaveLampSpecification :lampSpecificationId="control.selectedLampSpecificationId"></SaveLampSpecification>
  </el-dialog>
……
</tempalte>
<script>
import SaveLampSpecification from 'components/basicData/saveLampSpecification/saveLampSpecification.vue'

export default {
  components: {
    SaveLampSpecification
  },
  data() {
    ……
      control: {
        //保存灯具规格的显隐设置
        saveFormVisible: false,
        selectedLampSpecificationId: 1
      }
    }
  },
……
}
</script>

然后我们总体来说一下。首先是在子组件中,需要声明props以说明都有哪些属性可以使用。官网说着这里有个驼峰转横杠的自动命名转换。亲测,没有。然后,你声明的这个属性的访问方式其实和data里的是一样的,在我的代码中可以看到。但是,为了方便使用,我还是在data里声明了另一个属性,让它等于这个属性。这就是子组件的全部。而父组件,在使用的时候,将该属性和data的某个值进行绑定,也就形成了子组件和外部组件属性的关联。不过呢,这个时候你会发现一件事。当你改变外面的属性值后,里面的属性值是不会跟着改变的。这里需要在子组件中添加一个监听代码来搞定它:

  watch: {
    lampSpecificationId() {
      let self = this
      self.form.lampSpecificationId = self.lampSpecificationId
    }
  },

watch是vue的标准用法,我就不写这段代码的上下文了。总之,这样就完成了内外属性的关联绑定了。

重复打开弹窗遇到的坑

首先,无意中发现,默认情况下弹窗在任意非弹窗位置点击就关闭了。不过,找到属性,设置掉就可以了。另外我还遇见了另一个属性,即每次关闭都销毁相关的元素。调整后代码如下:

  <el-dialog title="添加灯具规格" 
    :visible.sync="control.saveFormVisible" width="500px" 
    :close-on-click-modal="false" 
    :destroy-on-close = "false">
    <SaveLampSpecification 
      :lampSpecificationId="control.selectedLampSpecificationId" 
      v-on:saved="saveLampSpecification">
    </SaveLampSpecification>
  </el-dialog>

这些呢,只能算是小坑。后面预见一个大坑。编辑数据的时候,除了第一次,没次打开都是上次的数据。这让我很震惊。我是将加载数据写在mounted里面的,难道这里有问题?开始得时候我以为是因为异步加载数据导致数据没更新,就查了半天vue更新数据不更新界面怎么弄。后来我发现,每次查询数据的日志是在我关闭界面的时候打的,于是我怀疑生命周期不是我预想的样子,于是把加载放到了上面写的watch里面,一切就都好了。

重点说明下destroy-on-close

上面的destroy-on-close属性,默认值就是false,只是我发现了它之后将它设置成了true。这将导致每次弹窗关闭后,其组件都会被销毁,下次打开就需要重新渲染。

  • 你在页面中是无法拿到这个组件的实例的,因为每次不显示的时候它都被销毁了。
  • 由于我一个组件同时干了创建和编辑的事情,而我又无法获取它的实例,于是上面的逻辑bug就很难处理。比如,我点开创建的时候,里面是空的。再点击一个编辑,加载了数据。关闭后再点开同一个编辑,就是空的。为什么呢?因为watch没有被触发,而组件又被销毁了,所以它理所当然的变成了初始的样子。而将这个设置为false后,一切都ok了。

综上所述,该属性的使用场景需要仔细考虑。

input的点击事件

很神奇的是,你会发现,在element ui的文档中,input是没有点击事件的。但是,其实这个事件我们用的并不少。于是找了下怎么加点击事件,结果说,你可以使用原生的click事件就可以了,代码大概如下:

<el-input v-on:click.native='clickSelectNode' ></el-input>

component 动态切换

在我的一个功能点上,我需要某个位置的组件根据不同的数据进行不同的展示。由于我数据都是按照产品型号进行划分的,所以不同的型号我做成了不同的组件。它们在动态组件component标签上进行切换。但是这种切换其实并不是说换一个属性就完了这么简单。为什么这么说呢?首先,假设,我有三个型号,每个型号有三十条数据。当我点开型号一的时候,它会触发mounted事件。但是,当我再点开另一条型号一的数据时,这个事件就不会再被触发了,vue把它存起来了。而在组件的生命周期中,我并没有找到准确得符合这个时机的事件。这就是它恶心的地方。
开始,我尝试了一种简单粗暴的解决方法,设置component的ref,然后换了组件后直接通过ref调用里面一个我约定了的方法。结果就是,我得等下次才能够调用到上次的ref。我没有找到确切的原因,猜测是因为我设置的是组件的名称,创建是异步的,所以我拿到的实际还是上一次的实例。
后来,我又查到了直接实例化组件。其实我成功了,但是,我只是成功调用到了方法,它里面注入的组件,组件里的数据都不存在,也就是它没有经过完善的生命周期。
最后,我回到了原始的处理思路。设置一个props传参,watch它,结合mounted事件,覆盖了所有的变化时机。

element ui 表格行单击和行内按钮重叠

鉴于我写的是个管理系统,表格就变成了交互的重点之一。为了减少需要使用的按钮,我将查看记录详情的操作变成了单击某行。但是,行间也有操作按钮。这个时候点击这些操作的按钮就变成了它们既执行自己的事件,也会执行行单击事件。这就有些蛋疼了。不过这件事是由于js的冒泡导致的。知道原因就好说了。但是吧,要说纯js,好说,这使用vue和element ui封装过的,怎么处理就有些讨厌了。不过查了下,还是有简单的方式的。在操作按钮那里套个div,注明组织点击事件传递即可,代码如下:

    <el-table-column
      label="操作">
      <template slot-scope="scope">
        <div @click.stop>
        <el-button v-on:click="modifyLampSpecification(scope.row.sensorId)">编辑</el-button>
        <el-button v-on:click="deleteLampSpecification(scope.row.sensorId)">删除</el-button>
        </div>
      </template>
    </el-table-column>

这里只展示了一个column的代码,不过应该足够看明白了。

element ui timepicker

其实这个组件使用起来相当简单。让我非常满意的是很灵活。之前我用layui的时候,选择时间,是无法配置只选择小时、分钟的,必须连秒一起选择。而这里,通过配置format就可以控制只有小时和分钟两个选择,棒棒哒。另外,值得一说的就是,设置它的值默认是js的时间对象,如果先用字符串,需要你设置value-format才可以。

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