纯前端导出excel,支持多sheet,多级表头,及合计

前端导出Excel相关链接:

纯前端利用 js-xlsx 之合并单元格
前端导出excel多级表头
前端导出多sheet的excel
vue-admin-templateUI库-前端导出excel

安装依赖

npm install --save xlsx file-saver

table 直接导出

支持删除不需要导出的列,多级表头,表尾合计

<template>
  <div>
    <el-button plain size="mini" @click="handleDownloadExcel">导出测试</el-button>
    <el-table :data="tableData" style="width: 100%" id="table">
      <el-table-column prop="date" label="日期" width="150"> </el-table-column>
      <el-table-column label="配送信息">
        <el-table-column prop="name" label="姓名" width="120"> </el-table-column>
        <el-table-column label="地址">
          <el-table-column prop="province" label="省份" width="120"> </el-table-column>
          <el-table-column prop="city" label="市区" width="120"> </el-table-column>
          <el-table-column prop="address" label="地址" width="300"> </el-table-column>
          <el-table-column prop="zip" label="邮编" width="120"> </el-table-column>
        </el-table-column>
      </el-table-column>
    </el-table>
    <el-table :data="tableData1" border style="width: 100%" id="table1">
      <el-table-column fixed prop="date" label="日期" width="150"> </el-table-column>
      <el-table-column prop="name" label="姓名" width="120"> </el-table-column>
      <el-table-column prop="province" label="省份" width="120"> </el-table-column>
      <el-table-column prop="city" label="市区" width="120"> </el-table-column>
      <el-table-column prop="address" label="地址" width="300"> </el-table-column>
      <el-table-column prop="zip" label="邮编" width="120"> </el-table-column>
      <el-table-column fixed="right" label="操作" width="100">
        <template slot-scope="scope">
          <el-button type="text" size="small">查看</el-button>
          <el-button type="text" size="small">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script>
import excel_export from './excelExport'
  export default {
    data() {
      return {
        sheetData: [
          {
            sheetName: 'sheet11',
            element: 'table',
            delCols: []
          },
          {
            sheetName: 'sheet22',
            element: 'table1',
            delCols: [0, 6] // 要删除的列
          }
        ],
        tableData: [
          {
            date: '2016-05-03',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-02',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-05',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-01',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-08',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-06',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-07',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          }
        ],
        tableData1: [
          {
            date: '2016-05-02',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1518 弄',
            zip: 200333
          },
          {
            date: '2016-05-04',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1517 弄',
            zip: 200333
          },
          {
            date: '2016-05-01',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1519 弄',
            zip: 200333
          },
          {
            date: '2016-05-03',
            name: '王小虎',
            province: '上海',
            city: '普陀区',
            address: '上海市普陀区金沙江路 1516 弄',
            zip: 200333
          }
        ]
      }
    },
    methods: {
      handleDownloadExcel() {
        excel_export({ sheetData: this.sheetData, exportType: 'table' })
      }
    }
  }
</script>

JsonData 形式导出

支持表头合并,表尾合计,自动列宽,两级表头(有需要多级表头可以再添加)

<template>
  <div>
    <el-button @click="handleDownloadExcel">导出</el-button>
  </div>
</template>
<script>
import excel_export from './excelExport'
  export default {
    data() {
      return {
        sheetData: [],
        list: [
          {
            id: 1,
            timestamp: 375676396994,
            author: 'Melissa',
            reviewer: 'Angela',
            title: 'Gxjxpr Jdfjwf Lxssr Deyonm Moxy Ryxhzp Gxtukv Bvnmvmlo',
            content_short: 'mock data',
            forecast: 29.75,
            importance: 2,
            type: 'EU',
            status: 'published',
            display_time: '1998-10-24 01:53:18',
            comment_disabled: true,
            pageviews: 1670,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 2,
            timestamp: 14742538820,
            author: 'Anthony',
            reviewer: 'Angela',
            title: 'Ueeislixa Heid Dvcspjcuw Rrjyqi Wsqgyjsw',
            content_short: 'mock data',
            forecast: 22.76,
            importance: 2,
            type: 'JP',
            status: 'published',
            display_time: '1990-03-21 05:30:52',
            comment_disabled: true,
            pageviews: 328,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 3,
            timestamp: 1315789932004,
            author: 'Sharon',
            reviewer: 'Brian',
            title: 'Oofunoeoob Xreaujp Ndl Xoin Nyp Iedlihw Jlxyvu Ropkbfi Rljxpyl',
            content_short: 'mock data',
            forecast: 36.57,
            importance: 1,
            type: 'CN',
            status: 'deleted',
            display_time: '1992-08-24 05:55:59',
            comment_disabled: true,
            pageviews: 1815,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 4,
            timestamp: 538203551911,
            author: 'Scott',
            reviewer: 'Joseph',
            title: 'Yxrxsh Vwstgjqg Xqokntiuh Yrhsc Iiiugfk Hmoc',
            content_short: 'mock data',
            forecast: 0.82,
            importance: 2,
            type: 'US',
            status: 'draft',
            display_time: '1982-10-19 21:06:25',
            comment_disabled: true,
            pageviews: 1575,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 5,
            timestamp: 1171719793154,
            author: 'Barbara',
            reviewer: 'Angela',
            title: 'Xvewsrudv Sojbjzssbn Nqcnslcl Fwtx Sertvsqnnf Rxnom Jxhrdyqv Tyvktmi Dqeypf',
            content_short: 'mock data',
            forecast: 22.84,
            importance: 2,
            type: 'JP',
            status: 'published',
            display_time: '1995-06-18 08:45:39',
            comment_disabled: true,
            pageviews: 1086,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 6,
            timestamp: 98663463269,
            author: 'Dorothy',
            reviewer: 'Sharon',
            title: 'Jaeiqv Murhokxs Jeocvo Mdwf Ktgobp',
            content_short: 'mock data',
            forecast: 84.76,
            importance: 2,
            type: 'EU',
            status: 'published',
            display_time: '1995-03-26 02:35:50',
            comment_disabled: true,
            pageviews: 1338,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 7,
            timestamp: 846870802127,
            author: 'Richard',
            reviewer: 'Timothy',
            title: 'Hriiunzrh Jvt Xxtvpzype Gqoe Lkppdfbqh Txrfpgn Ykrloytzg Mgirqldy',
            content_short: 'mock data',
            forecast: 35.18,
            importance: 3,
            type: 'CN',
            status: 'draft',
            display_time: '1973-07-02 19:02:43',
            comment_disabled: true,
            pageviews: 1335,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          },
          {
            id: 8,
            timestamp: 590582195083,
            author: 'George',
            reviewer: 'Helen',
            title: 'Tgcwifqcw Gogg Jsuypoa Hutu Rofnrslx Tntc Xehodrw Qrnsro Bcyilwymy',
            content_short: 'mock data',
            forecast: 86.58,
            importance: 2,
            type: 'US',
            status: 'draft',
            display_time: '1980-06-28 20:47:17',
            comment_disabled: true,
            pageviews: 4937,
            image_uri: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3',
            platforms: ['a-platform']
          }
        ],
        rolesList: [
          {
            userId: 123,
            userNum: 'admin', // 用户账号
            userName: 'admin', // 用户名称
            userDescription: '这个账户是超级管理员账户'
          },
          {
            userId: 124,
            userNum: 'wxf123', // 用户账号
            userName: '张三', // 用户名称
            userDescription: '这个账户是一级管理员账户'
          },
          {
            userId: 125,
            userNum: 'cz123', // 用户账号
            userName: '李四', // 用户名称
            userDescription: '这个账户是二级管理员账户'
          }
        ]
      }
    },
    methods: {
      handleDownloadExcel() {
        excel_export({ sheetData: this.sheetData, exportType: 'json' })
      }
    },
    created() {
      this.sheetData = [
        {
          tHeader: ['Id', 'Title', 'Author', 'Readings', 'Date'], // sheet表一头部
          filterVal: ['id', 'title', 'author', 'pageviews', 'display_time'], // 表一的数据字段
          data: this.list, // 表一的整体json数据
          merge: ['A1:A2', 'B1:E1'], // 表头合并
          multiHeader: ['序号', '信息'], // 合并表头的数据
          sheetName: 'sheet11', // 表一的sheet名字
          sum: ['合计', '', '', 14084, ''] //表尾合计
        },
        {
          tHeader: ['序号', '标题', '作者', '服务'],
          filterVal: ['id', 'title', 'author', 'reviewer'],
          data: this.list,
          merge: ['A1:A2', 'B1:C1'],
          multiHeader: ['序号', '其他'],
          sheetName: 'sheet22'
        },
        {
          tHeader: ['序号', '名字', '描述'],
          filterVal: ['userId', 'userName', 'userDescription'],
          data: this.rolesList,
          merge: [],
          multiHeader: [],
          sheetName: 'sheet33'
        }
      ]
    }
  }
</script>

核心代码

/* eslint-disable */
import { saveAs } from 'file-saver'
import XLSX from 'xlsx'
function generateArray(table) {
  var out = []
  var rows = table.querySelectorAll('tr')
  var ranges = []
  for (var R = 0; R < rows.length; ++R) {
    var outRow = []
    var row = rows[R]
    var columns = row.querySelectorAll('td')
    for (var C = 0; C < columns.length; ++C) {
      var cell = columns[C]
      var colspan = cell.getAttribute('colspan')
      var rowspan = cell.getAttribute('rowspan')
      var cellValue = cell.innerText
      if (cellValue !== '' && cellValue == +cellValue) cellValue = +cellValue

      //Skip ranges
      ranges.forEach(function(range) {
        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null)
        }
      })

      //Handle Row Span
      if (rowspan || colspan) {
        rowspan = rowspan || 1
        colspan = colspan || 1
        ranges.push({ s: { r: R, c: outRow.length }, e: { r: R + rowspan - 1, c: outRow.length + colspan - 1 } })
      }
      //Handle Value
      outRow.push(cellValue !== '' ? cellValue : null)

      //Handle Colspan
      if (colspan) for (var k = 0; k < colspan - 1; ++k) outRow.push(null)
    }
    out.push(outRow)
  }
  return [out, ranges]
}

function datenum(v, date1904) {
  if (date1904) v += 1462
  var epoch = Date.parse(v)
  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
}

function sheet_from_array_of_arrays(data, opts) {
  var ws = {}
  var range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } }
  for (var R = 0; R != data.length; ++R) {
    for (var C = 0; C != data[R].length; ++C) {
      if (range.s.r > R) range.s.r = R
      if (range.s.c > C) range.s.c = C
      if (range.e.r < R) range.e.r = R
      if (range.e.c < C) range.e.c = C
      var cell = { v: data[R][C] }
      if (cell.v == null) continue
      var cell_ref = XLSX.utils.encode_cell({ c: C, r: R })

      if (typeof cell.v === 'number') cell.t = 'n'
      else if (typeof cell.v === 'boolean') cell.t = 'b'
      else if (cell.v instanceof Date) {
        cell.t = 'n'
        cell.z = XLSX.SSF._table[14]
        cell.v = datenum(cell.v)
      } else cell.t = 's'

      ws[cell_ref] = cell
    }
  }
  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range)
  return ws
}

function Workbook() {
  if (!(this instanceof Workbook)) return new Workbook()
  this.SheetNames = []
  this.Sheets = {}
}

function s2ab(s) {
  var buf = new ArrayBuffer(s.length)
  var view = new Uint8Array(buf)
  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
  return buf
}
function formatJson(filterVal, jsonData) {
  return jsonData.map(v => filterVal.map(j => v[j]))
}
/**
 * 
 * @param {sheetData} 数据格式如下
 * [
      {
        sheetName: 'sheet1', // sheet的名字
        element: null, // 要导出的table的id,DOM也可以
        delCols: [] // 要删除的列,如操作列
      }
    ]
 */
export function export_table_to_excel({ sheetData, filename, bookType }) {
  const xlsxParam = { raw: true } // 导出的内容只做解析,不进行格式转换
  if (sheetData.length < 1) {
    console.log('请传入数据')
    return
  }
  const workbook = XLSX.utils.book_new()
  sheetData.forEach((sheet, index) => {
    let table
    const { element, delCols = [] } = sheet
    if (element) {
      if (typeof element === 'string') {
        table = document.querySelector(`#${element}`).cloneNode(true)
      } else {
        table = element.cloneNode(true)
      }
      // 判断要导出的节点中是否有fixed的表格,如果有,转换excel时先将该dom移除,
      const fix = table.querySelectorAll('.el-table__fixed, .el-table__fixed-right')
      if (fix.length > 0) {
        fix.forEach(ele => {
          table.removeChild(ele)
        })
      }
      // 需要删除的项
      if (delCols && delCols.length > 0) {
        const thead_tr = table.querySelectorAll('.el-table__header-wrapper thead tr')
        const thead_th = thead_tr[0].querySelectorAll('th')
        delCols.forEach(i => {
          thead_tr[0].removeChild(thead_th[i])
        })
        const tbody_tr = table.querySelectorAll('.el-table__body-wrapper tbody tr')
        tbody_tr.forEach(item => {
          const thead_td = item.querySelectorAll('td')
          delCols.forEach(i => {
            item.removeChild(thead_td[i])
          })
        })
      }
      // const ws = XLSX.utils.table_to_sheet(table)
      const ws = XLSX.utils.table_to_sheet(table, xlsxParam)
      XLSX.utils.book_append_sheet(workbook, ws, sheet.sheetName || `sheet${index + 1}`)
    }
  })
  /* get binary string as output */
  const wbout = XLSX.write(workbook, { bookType, bookSST: true, type: 'array' })
  try {
    saveAs(new Blob([wbout], { type: 'application/octet-stream' }), `${filename}.${bookType}`)
  } catch (e) {
    if (typeof console !== 'undefined') console.log(e, wbout)
  }
}
/**
 * 
 * @param {sheetData} 数据格式如下
 * [
      {
        tHeader: [], // 表头
        filterVal: [], // 表头对应的字段
        data: [], // 数据源
        multiHeader: [], // 一级表头
        merge: [], // 表头合并数据
        sheetName: '', // sheet的名称
        sum: [] // 表尾统计数据
       }
    ]
 */

export function export_json_to_excel({ sheetData, filename, bookType, autoWidth }) {
  /* original data */
  const header = []
  const data = []
  const sheetnames = []
  const merges = []
  const multiHeader = []
  const sum = []
  let item = null
  for (let i = 0; i < sheetData.length; i++) {
    item = sheetData[i]
    header.push(item.tHeader || [])
    data.push(formatJson(item.filterVal, item.data) || [])
    sheetnames.push(item.sheetName || '')
    merges.push(item.merge || [])
    multiHeader.push(item.multiHeader || [])
    sum.push(item.sum || [])
  }

  // 第二行表头
  for (let i = 0; i < header.length; i++) {
    if (header[i].length > 0) {
      data[i].unshift(header[i])
    }
  }

  // 第一行表头
  if (multiHeader.length > 0) {
    for (let n = 0; n < multiHeader.length; n++) {
      if (multiHeader[n].length > 0) {
        data[n].unshift(multiHeader[n])
      }
    }
  }

  if (sum.length > 0) {
    for (let index = 0; index < sum.length; index++) {
      if (sum[index].length > 0) {
        data[index].push(sum[index])
      }
    }
  }

  let ws_name = sheetnames
  let wb = new Workbook(),
    ws = []
  for (let j = 0; j < header.length; j++) {
    ws.push(sheet_from_array_of_arrays(data[j]))
  }

  if (merges.length > 0) {
    // 表头合并
    merges.forEach((merge, i) => {
      if (merge.length > 0) {
        merge.forEach(item => {
          if (!ws[i]['!merges']) ws[i]['!merges'] = []
          ws[i]['!merges'].push(XLSX.utils.decode_range(item))
        })
      }
    })
  }

  if (autoWidth) {
    /*设置worksheet每列的最大宽度*/
    let colWidth = []
    for (let m = 0; m < header.length; m++) {
      colWidth.push(
        data[m].map(row =>
          row.map(val => {
            /*先判断是否为null/undefined*/
            if (val == null) {
              return {
                wch: 10
              }
            } else if (val.toString().charCodeAt(0) > 255) {
              /*再判断是否为中文*/
              return {
                wch: val.toString().length * 2
              }
            } else {
              return {
                wch: val.toString().length
              }
            }
          })
        )
      )
    }
    //以倒数第二行为初始值,避开表头行和合计行,以免影响宽度计算
    let result = []
    for (let k = 0; k < colWidth.length; k++) {
      result[k] = colWidth[k][colWidth[k].length - 2]
      for (let i = 0; i < colWidth[k].length; i++) {
        for (let j = 0; j < colWidth[k][i].length; j++) {
          if (result[k][j]['wch'] < colWidth[k][i][j]['wch']) {
            result[k][j]['wch'] = colWidth[k][i][j]['wch']
          }
        }
      }
    }
    // 分别给sheet表设置宽度
    for (let l = 0; l < result.length; l++) {
      ws[l]['!cols'] = result[l]
    }
  }

  /* add worksheet to workbook */
  for (let a = 0; a < header.length; a++) {
    wb.SheetNames.push(ws_name[a])
    wb.Sheets[ws_name[a]] = ws[a]
  }

  let wbout = XLSX.write(wb, {
    bookType: bookType,
    bookSST: false,
    type: 'binary'
  })
  saveAs(
    new Blob([s2ab(wbout)], {
      type: 'application/octet-stream'
    }),
    `${filename}.${bookType}`
  )
}

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