Vue根据vue-router生成菜单导航栏简单实现

在使用 vue-element-admin 后,我觉得它通过 vue-router 生成导航菜单功能挺有意思的,所以阅读了一下他的源码,自己简单的实现一下。

文章主要关注如何生成导航菜单,以及子菜单的展开收缩管理,还有点击菜单的激活状态管理。为了避免文章过长,所以就不写登录页、图标以及动态添加路由了。

准备工作

在干活之前需要把要用到的 router、布局准备好

准备 router

要根据 router 生成菜单导航栏,那么一定是需要一个 router 的,这里新建一个 router

/src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    component: () => import('../views/Home'),
    meta: {
      title: '八大菜系介绍'
    }
  },
  {
    path: '/luCai',
    component: () => import('../views/luCai'),
    meta: {
      title: '鲁菜'
    },
    children: [
      {
        path: 'history',
        component: () => import('../views/luCai/history/index'),
        meta: {
          title: '发展历史'
        },
        children: [
          {
            path: 'qinAndHan',
            component: () => import('../views/luCai/history/qinAndHanDynasties'),
            meta: {
              title: '秦汉时期'
            }
          },
          {
            path: 'northernWei',
            component: () => import('../views/luCai/history/northernWeiDynasty'),
            meta: {
              title: '北魏时期'
            }
          }
        ]
      },
      {
        path: 'features',
        component: () => import('../views/luCai/features'),
        meta: {
          title: '风味特色'
        }
      }
    ]
  },
  {
    path: '/chuanCai',
    component: () => import('../views/chuanCai'),
    meta: {
      title: '川菜'
    },
    children: [
      {
        path: 'introduction',
        component: () => import('../views/chuanCai/introduction'),
        meta: {
          title: '川菜概论'
        }
      },
      {
        path: 'history',
        component: () => import('../views/chuanCai/history'),
        meta: {
          title: '发展历史'
        }
      }
    ]
  }
]

const router = new VueRouter({
  routes
})

export { router, routes }

meta 里面的 title 是菜单显示的名字

准备布局

这里还是采用左侧菜单栏,右侧内容的布局,顶部的 header 不管他。

/src/Layout.vue

<template>
  <div id="app">
    <simple-menu/>
    <div class="container">
      <router-view/>
    </div>
  </div>
</template>
<script>
import SimpleMenu from "./components/menu/Menu"
export default {
  components: { SimpleMenu }
}
</script>
<style>
body {
  margin: 0;
}
#app {
  height: 100vh;
}
.container {
  margin-left: 200px;
  height: 100%;
  background: #f5f7fa;
  padding: 10px;
  box-sizing: border-box;
}
</style>

这里的 Menu 还是空壳子,先放上代码吧,主要是一点简单的样式

/src/components/menu/Menu.vue

<template>
  <div class="menu">

  </div>
</template>

<script>
export default {
  props: {},
  data() {
    return {}
  },
  methods: {},
  computed: {}
}
</script>

<style scoped>
.menu {
  user-select: none;
  width: 200px;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  box-shadow: 5px 5px 5px rgba(204, 204, 204, 0.23);
  background: white;
}
</style>

那么准备就绪了,就看看效果图吧!

布局效果图

PS:有 children 的路由页面都只有简单的<router-view>,比如 path 为 '/luCai' 的路由 component,它的文件内容就是

<template>
  <router-view/>
</template>

开始生成菜单

通过分析我发现菜单的主要组成部分分为 3 个组件:

  1. 根菜单组件 -> 提供收缩展开状态管理。
  2. 子菜单组件 -> 将 router 下的 children 收起来,类似于文件夹的作用。另外提供收起展开动画。
  3. 菜单条目组件 -> 导航路由,点击此组件切换路由。

整个实现我也给分成 3 步:

  1. 递归生成树状菜单
  2. 实现树状菜单的展开收起管理
  3. 实现菜单的激活状态管理

递归生成树状菜单

所谓递归,其实就是我们喜欢的俄罗斯套娃!为了方便描述,我把根菜单组件取名为 Menu.vue,把子菜单组件取名为 SubMenu.vue,菜单条目组件叫做 MenuItem.vue。

他们之间的关系就是 Menu 会遍历 routers 列表,判断遍历的当前 item 是否有 children,如果有则渲染 SubMenu 组件,如果没有则渲染 MenuItem 组件。这里的 item 就是 route 对象。

当 Menu 渲染 SubMenu 时,会传两个 prop 参数,就是当前 item 以及 basePath,basePath的值为 item.path

当 SubMenu 被渲染出来后,会拿到 item,然后遍历 item.children,接着会在循环中判断 children 的 item,这里另外取个名字叫 itemX,免得跟 props 的 item 搞混了。

SubMenu 在循环中判断 itemX 是否有 children?如果有,则渲染 SubMenu 组件,并且把接收到的 basePath 和 itemX.path 拼接起来传给 SubMenu 的 basePath 参数。到这里就算是一个递归调用了,SubMenu 渲染了 SubMenu 渲染了 SubMenu 渲染....直到接收到的 item 没有 children。

如果 itemX 没有 children,则表示这是一个完整的路由了,渲染 MenuItem。MenuItem 的 path 参数为 basePath + itemX.path。经过层层拼接,这样 MenuItem 就可以用 path 进行路由跳转了。

整个流程就这么走完了,可能你还有点懵,那么来张图吧!

递归生成菜单

还看不懂也没关系,这里有香喷喷的代码

/src/components/menu/MenuItem.vue

<template>
    <router-link :to="path">
      <div class="menu-item">{{ title }}</div>
    </router-link>
</template>

<script>
export default {
  name: 'menu-item',
  props: {
    path: { // 唯一的routerPath
      type: String,
      required: true
    },
    title: { // 标题
      type: String,
      required: true
    }
  },
  data() {
    return {}
  },
  methods: {}
}
</script>

<style scoped>
.menu-item {
  padding: 20px;
  color: rgb(153, 153, 153);
  user-select: none
}
.menu-item:hover {
  background: rgb(244, 244, 244);
}
</style>

这个就很简单,点击它就会路由跳转,鼠标放上去背景色变灰,还是要有点美观的。

下面有请 SubMenu 上场!

/src/components/menu/SubMenu.vue

<template>
  <div>
    <div
      class="title"
    >
      <!-- 标题 -->
      <a href="javascript:void(0)">{{ item.meta.title }}</a>
      <!-- 箭头符号 -->
      <svg class="icon" viewBox="0 0 1024 1024" width="16" height="16">
        <path d="M472.064 751.552 72.832 352.32c-22.08-22.08-22.08-57.792 0-79.872 22.016-22.016 57.792-22.08 79.872 0L512 631.744l359.296-359.296c22.016-22.016 57.792-22.08 79.872 0 22.08 22.08 22.016 57.792 0 79.872l-399.232 399.232C529.856 773.568 494.144 773.568 472.064 751.552z" fill="#999999"/>
      </svg>
    </div>
    <div
      class="children"
    >
      <template v-for="itemX in item.children">
        <sub-menu
          v-if="itemX.children && itemX.children.length>1"
          :item="itemX"
          :base-path="resolvePath(itemX.path)"
          :key="itemX.path"
        />
        <menu-item
          v-else
          :path="resolvePath(itemX.path)"
          :title="itemX.meta.title"
          :key="itemX.path"
          />
      </template>
    </div>
  </div>
</template>

<script>
import path from 'path'
import MenuItem from "./MenuItem"
export default {
  components: { MenuItem },
  name: 'sub-menu', // 一定要写 name,不然递归调用自己会报错
  props: {
    item: { // route object
      type: Object,
      required: true
    },
    basePath: { // 上级 path
      type: String,
      default: ''
    }
  },
  data() {
    return {}
  },
  methods: {
    // 将 routePath 和 basePath 拼接起来
    resolvePath(routePath) {
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>

<style scoped>
.title {
  padding: 20px;
  color: rgb(153, 153, 153);
}
.title:hover {
  background: rgb(244, 244, 244);
}
.children {
  padding-left: 20px;
  background: rgb(244, 244, 244);
}
.icon {
  float: right;
  color: #999999;
}
</style>

SubMenu 的代码看着就很简单,核心代码就是一个 v-for,当遍历到有 children 时,就把自己渲染出来,以渲染一个子菜单,否则渲染 MenuItem。写到这里我甚至想把之前的文字说明删掉了。

当一个机器的零件造好了,我们就可以把他们组装起来了。组装成 Menu.vue

/src/components/menu/Menu.vue

<template>
    <div class="menu">
        <template v-for="item in routes">
          <sub-menu
            v-if="item.children && item.children.length > 1"
            :item="item"
            :base-path="item.path"
            :key="item.path"
          />
          <menu-item
            v-else
            :path="item.path"
            :title="item.meta.title"
            :key="item.path"
            />
        </template>
    </div>
</template>

<script>
import { routes } from '../../router'
import SubMenu from './SubMenu'
import MenuItem from './MenuItem'

export default {
  name: 'simple-menu',
  components: { MenuItem, SubMenu },
  props: {},
  data() {},
  methods: {
  },
  computed: {
    routes() {
      return routes
    }
  }
}
</script>

<style scoped>
.menu {
    user-select: none;
    width: 200px;
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    box-shadow: 5px 5px 5px rgba(204, 204, 204, 0.23);
    background: white;
    overflow-x: hidden;
}
</style>

是的,你没看错,这就是一个循环,跟前面写的完全一致。这里的 routes 是要渲染的路由列表,可能有的同学觉得没必要再弄个 computed 来返回,但是如果不弄的话,就会报 undefind,并且还能够在这里对 routes 做些操作。比如只渲染列表里的某一项之类的。

好了,这个时候我们运行来看看效果

生成效果图

管理展开收缩状态

如果菜单全部都展开来显示的话,很占空间,所以我们需要让他把子菜单都收起来。那么怎么管理展开收起状态呢?

可以把要展开的 SubMenu 的 basePath 添加进 Menu 的 openedSubMenus 里,当 SubMenu 的 basePath 在里面,则表示自己是展开,否则是收起的。

之所以放进 Menu 里而不是它的父级组件里,是因为方便统一管理,在关闭一个 SubMenu 时,同时还需要把 SubMenu 的子 SubMenu 给关闭。

那么子组件要如何操作 Menu 的 openedSubMenus 呢?我们可以利用 vue 的 provide/inject 特性。

provide/inject 允许一个组件提供(provide)一些属性给子组件注入(inject)使用。子组件的子组件也就孙子组件、玄孙组件等等都能用,无论有多深层都能用。

当点击 SubMenu 时就让 Menu 判断是展开还是收起。如果是展开就把被点击的 basePath 添加到 openedSubMenus 中,否则移除。

思路有了就开始行动写代码,我们先完成控制中心代码 Menu 组件。

/src/components/menu/Menu.vue

<template>
    <div class="menu">
        <template v-for="item in routes">
          <sub-menu
            v-if="item.children && item.children.length > 1"
            :item="item"
            :base-path="item.path"
            :key="item.path"
          />
          <menu-item
            v-else
            :path="item.path"
            :title="item.meta.title"
            :key="item.path"
            />
        </template>
    </div>
</template>

<script>
import { routes } from '../../router'
import SubMenu from './SubMenu'
import MenuItem from './MenuItem'

export default {
  name: 'simple-menu',
  components: { MenuItem, SubMenu },
  provide() {
    return {
      rootMenu: this // 把自己提供给子组件
    }
  },
  props: {},
  data() {
    return {
      openedSubMenus: [] // 已展开的子菜单index
    }
  },
  methods: {
    // 处理子菜单点击事件
    handleClickSubMenu(basePath) {
      if (this.openedSubMenus.includes(basePath)) {
        this.closeSubMenu(basePath)
      } else {
        this.openSubMenu(basePath)
      }
    },

    // 打开子菜单
    openSubMenu(basePath){
      this.openedSubMenus.push(basePath)
    },

    // 关闭子菜单
    closeSubMenu(basePath) {
      this.openedSubMenus.splice(this.openedSubMenus.indexOf(basePath), 1)
      // 关闭 path 下的子菜单
      this.openedSubMenus = this.openedSubMenus.filter(item => item.indexOf(basePath+'/') !== 0)
    }
  },
  computed: {
    routes() {
      return routes
    }
  }
}
</script>

<style scoped>
.menu {
    user-select: none;
    width: 200px;
    height: 100%;
    position: fixed;
    top: 0;
    left: 0;
    box-shadow: 5px 5px 5px rgba(204, 204, 204, 0.23);
    background: white;
    overflow-x: hidden;
    cursor: pointer;
}
</style>

这里主要是通过 provide 将 Menu 组件实例提供给了所有后代组件。后代组件只需要通过 inject 注入就可以调用它的方法和 data,同时在 data 里创建了一个空列表 openedSubMenus,当 SubMenu 组件的 basePath 在这个列表里就代表该 SubMenu 是展开的。最后我们创建了三个函数,看注释就知道是干嘛用的,这里有可能让同学们疑惑的地方就是将要收起的子菜单下的所有子菜单一起收起的代码,也就是这一段:

this.openedSubMenus = this.openedSubMenus.filter(item => item.indexOf(basePath+'/') !== 0)

众所周知,我们的 basePath 是一层一层的拼接的,那么就会有一个特点:“即子菜单的 basePath 是子菜单下的所有后代子菜单的 basePath 的前半部分”,举个例子,我们有个子菜单,他的 basePath 是 /animal ,这个子菜单下的子菜单的 route.path 是 dog,则拼接出来的就是 /animal/dog ,当我们要关闭 /animal 这个子菜单下的所有子菜单时,就只需要把以 /animal/ 开头的 basePath 从 openedSubMenus 里筛选出去就好了,所以这里用了 filter 函数进行筛选。之所以要加在后面加上 / 符号,是因为要避免匹配到 /animals 这样的字符串。

接下来是 SubMenu

/src/components/menu/SubMenu.vue

<template>
  <div>
    <div
      class="title"
      @click="handleClick"
    >
      <!-- 标题 -->
      <a href="javascript:void(0)" style="user-select: none">{{ item.meta.title }}</a>
      <!-- 箭头符号 -->
      <svg class="icon" viewBox="0 0 1024 1024" width="16" height="16">
        <path
          d="M472.064 751.552 72.832 352.32c-22.08-22.08-22.08-57.792 0-79.872 22.016-22.016 57.792-22.08 79.872 0L512 631.744l359.296-359.296c22.016-22.016 57.792-22.08 79.872 0 22.08 22.08 22.016 57.792 0 79.872l-399.232 399.232C529.856 773.568 494.144 773.568 472.064 751.552z"
          fill="#999999"/>
      </svg>
    </div>
    <div
      v-show="expanded"
      class="children"
    >
      <template v-for="itemX in item.children">
        <sub-menu
          v-if="itemX.children && itemX.children.length>1"
          :item="itemX"
          :base-path="resolvePath(itemX.path)"
          :key="itemX.path"
        />
        <menu-item
          v-else
          :path="resolvePath(itemX.path)"
          :title="itemX.meta.title"
          :key="itemX.path"
        />
      </template>
    </div>
  </div>
</template>

<script>
import path from 'path'
import MenuItem from './MenuItem'

export default {
  components: {MenuItem},
  name: 'sub-menu', // 一定要写 name,不然递归调用自己会报错
  inject: ['rootMenu'],
  props: {
    item: { // route object
      type: Object,
      required: true
    },
    basePath: { // 上级 path
      type: String,
      default: ''
    }
  },
  data() {
    return {}
  },
  computed: {
    // 是否展开
    expanded() {
      return this.rootMenu.openedSubMenus.includes(this.basePath)
    }
  },
  methods: {
    // 将 routePath 和 basePath 拼接起来
    resolvePath(routePath) {
      return path.resolve(this.basePath, routePath)
    },
    // 处理展开收起事件
    handleClick() {
      this.rootMenu.handleClickSubMenu(this.basePath)
    }
  }
}
</script>

<style scoped>
  .title {
    padding: 20px;
    color: rgb(153, 153, 153);
  }

  .title:hover {
    background: rgb(244, 244, 244);
  }

  .children {
    padding-left: 20px;
    background: rgb(244, 244, 244);
  }

  .icon {
    float: right;
    color: #999999;
  }
</style>

SubMenu 的代码就很简单了。

第一步:injiect 注入 rootMenu。

第二步:给组件的标题添加点击事件,调用 rootMenu 的 handleClickSubMenu 方法,并把自己的 basePath 传过去。

第三步:新增计算属性 expanded,计算自己的 basePath 是否在 openedSubMenus 中,存在就表示是展开,反之则是收起。

第四步:使用 v-show 来绑定 expanded 计算属性。要用 v-show,不要用 v-if。以避免重复渲染造成性能浪费。

OK,到了这里我们就可以展开收起了,看看效果先

收起展开管理

现在还没有展开收起的动画,这一篇文章因为已经很长了,所以我打算另外再写一篇来专门讲展开收起动画。

激活状态管理

终于到了最后的激活状态管理了,这也是最简单的一部分,主要分四步走:

  1. MenuItem 新增一个计算属性 active 判断自己的 path 跟 $router.path 是否一致,一致则表示激活。
  2. SubMenu 新增一个对象 childes 用来存 MenuItem 和 SubMenu 的实例。
  3. SubMenu 和 MenuItem 在挂载后把自己的实例添加到父组件的 childes 中。
  4. SubMenu 新增一个计算属性 active,遍历 childes 对象,判断是否有 active 为 true 的实例。如果有,则 active 为 true。

先完成第一步,找到 MenuItem.vue,新增一个 .active 样式,激活时让字体变成橘色:
/src/components/menu/MenuItem.vue

.menu-item {
  padding: 20px;
  color: rgb(153, 153, 153);
  user-select: none
}
.menu-item:hover {
  background: rgb(244, 244, 244);
}
.active {
  color: orange;
}

然后新增一个计算属性 active:

/src/components/menu/MenuItem.vue

computed: {
  // 当前菜单是否激活
  active() {
    return this.$route.path === this.path
  }
}

修改 div,通过 :class 进行样式绑定

/src/components/menu/MenuItem.vue

<template>
    <router-link :to="path">
      <div class="menu-item" :class="{ 'active': active }">{{ title }}</div>
    </router-link>
</template>

第一步完成,进入第二步,打开 SubMenu.vue 修改 data

/src/components/menu/SubMenu.vue

data() {
  return {
    childes: {}
  }
}

新增两个方法来添加/删除 child

/src/components/menu/SubMenu.vue

methods: {
  // ...其他已有方法
  
  addChild(index, child) {
    this.$set(this.childes, index, child)
  },
  removeChild(index) {
    delete this.childes[index]
  }
},

到第三步,挂载后将自己添加到父组件的 childes 中,销毁后要从 childes 中移除

/src/components/menu/MenuItem.vue

mounted() {
  // 如果父级存在 addChild 方法则将自己添加进去
  if (this.$parent.hasOwnProperty('addChild')) {
    this.$parent.addChild(this.path, this)
  }
},
destroyed() {
  // 如果父级存在 removeChild 方法则将自己从中移除
  if (this.$parent.hasOwnProperty('removeChild')) {
    this.$parent.removeChild(this.path)
  }
}

/src/components/menu/SubMenu.vue

mounted() {
  // 如果父级存在 addChild 方法则将自己添加进去
  if (this.$parent.hasOwnProperty('addChild')) {
    this.$parent.addChild(this.basePath, this)
  }
},
destroyed() {
  // 如果父级存在 removeChild 方法则将自己从中移除
  if (this.$parent.hasOwnProperty('removeChild')) {
    this.$parent.removeChild(this.basePath)
  }
}

第四步,SubMenu 遍历 childes 计算自己是否应该为激活状态。

/src/components/menu/SubMenu.vue

computed: {
  // 是否展开
  expanded() {
    return this.rootMenu.openedSubMenus.includes(this.basePath)
  },
  // 是否激活
  active() {
    let active = false
    Object.keys(this.childes).forEach(key => {
      if (this.childes[key].active) {
        active = true
      }
    })
    return active
  }
}

最后就是把 css 样式绑定了

/src/components/menu/SubMenu.vue

<div
  class="title"
  @click="handleClick"
  :class="{ 'active': active }"
>
...
</div>
<style scoped>
  .title {
    padding: 20px;
    color: rgb(153, 153, 153);
  }
  .active {
    color: orange;
  }
  ...
</style>

到这里就结束了,来看看效果吧!

激活状态管理

结语

现在实现了展开收起管理和激活状态管理,就差一个动画了,当然还有隐藏路由这样的操作,这些后面单独出文章来更新。先来看看带展开收起动画的效果吧。

带展开收起动画

这里箭头的旋转动画也还没做,不过

“罗马不是一天建成的”(Rome was not built in a day)

所以更多的细节功能后期会慢慢补充,现在先挖个坑。

感谢看官的耐心阅读,敬请期待展开收起动画篇😘

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

推荐阅读更多精彩内容