JXL组件:组件库引用的 mixin.js、util.js 和 support.js 文件代码

mixin.js 混入

在每个组件库混入这些基础公共方法,不再手动按需导入。

import {
  _get,
  _typeof,
  _typein,
  isValid,
  isVoid,
  friendly,
  isHidden,
  isVisible,
  isDisabled
} from '@/components/libs/util'
const mixin = {
  methods: {
    _typeof,
    _typein,
    _get,
    isVoid,
    isValid,
    friendly,
    isHidden,
    isVisible,
    isDisabled
  }
}
export default mixin

util.js 工具

定义基础公共方法。

import { getDataType } from '@/utils/libs/support'

/**
 * 获得参数的数据类型
 * @param text 目标文本
 * @returns {undefined|null|string|Function|Number|Array|Object|Boolean|BigInt|Symbol}
 */
export function _typeof(text) {
  const textType = typeof text
  switch (textType) {
    case 'undefined':
      return undefined
    case 'function':
      return Function
    case 'number':
      return Number
    case 'object':
      if (text === null) return null
      if (Array.isArray(text)) return Array
      return Object
    case 'boolean':
      return Boolean
    case 'string':
      return String
    case 'bigint':
      return BigInt
    case 'symbol':
      return Symbol
  }
}

/**
 * 校验数据类型
 * @param text 目标文本
 * @param type 预期类型
 * @returns {void, boolean}
 */
export function _typein(text, type) {
  const TYPE = _typeof(type)
  const TEXT = _typeof(text)
  if (TYPE === Array) return type.includes(TEXT)
  return type === TEXT
}

/**
 * 判断文本是否无效
 * @param text 待检测的文本
 * @param includeZero 是否包括零,默认false
 * @returns {boolean}
 */
export function isVoid(text, includeZero = false) {
  if (text === null || text === undefined || text === '') return true
  return includeZero && (text === 0)
}

/**
 * 数据是否有效
 * @param text 目标文本
 * @returns {boolean}
 */
export function isValid(text) {
  return !isVoid(text)
}

/**
 * 对象多层级取值,避免报错
 * @param data 取值的对象
 * @param path 取值的路径
 * @returns {undefined}
 * @private
 */
export function _get(data, path) {
  if (isVoid(data)) return undefined
  if (_typeof(path) !== String) return undefined
  const pathArr = path.split('.')
  let result = data
  for (let i = 0; i < pathArr.length; i++) {
    if (result[pathArr[i]] === undefined) return undefined
    result = result[pathArr[i]]
  }
  return result
}

/**
 * 友好展示文本信息
 * @param text 待展示的文本
 * @param displace 当文本无效时,取代展示的文本
 * @param other 当文本有效时,要求展示其他文本
 * @param added 补充显示的条件,默认值true
 * @returns {string|*}
 */
export function friendly(text, displace = '', added = true, other) {
  if (isVoid(text) && added) return displace
  return other !== undefined ? other : text
}

/**
 * 防止无效的输入
 * 去掉字符串左边的空格,连续的空格替换为一个空格
 * @param params
 * -params.text 需要去除多余空格的文本,默认值“”
 * -params.trimStart 是否需要去除文本左边的空格,默认值true
 * -params.trimEnd 是否需要去除文本右边的空格,默认值false
 * -params.trimAll 是否需要去除文本所有的空格,默认值false
 * @returns {*}
 */
export function preventVoidInput(params) {
  const dataType = getDataType(params)
  if (dataType === 'Sting') {
    params = { text: params }
  } else if (dataType !== 'object') {
    return console.error(`params: 期望接收一个“object”参数,意外的获得一个“${dataType}”参数!`)
  }
  const { trimStart = true, trimEnd = false, trimAll = false } = params // 参数对象解构
  let { text = '' } = params // 参数对象解构
  if (isVoid(text)) return ''
  if (trimAll) return text.replace(/\s+/g, '')
  if (trimStart) {
    text = text.trimStart()
  }
  if (trimEnd) return text.trimEnd()
  return text.replace(/\s+/g, ' ')
}

/**
 * 是否隐藏
 * @param item 当前级
 * @param parent 父级
 * @returns {boolean}
 */
export function isHidden(item, parent = null) {
  if (_typeof(item) !== Object) return false
  if (_typeof(item.hidden) === Function) return Boolean(item.hidden(parent || item))
  return Boolean(item.hidden)
}

/**
 * 是否可见
 * @param item 当前级
 * @param parent 父级
 * @returns {boolean}
 */
export function isVisible(item, parent = null) {
  return !isHidden(item, parent)
}

/**
 * 是否禁用
 * @param item 当前级
 * @param parent 父级
 * @returns {boolean}
 */
export function isDisabled(item, parent = null) {
  if (_typeof(item) !== Object) return false
  if (_typeof(item.disabled) === Function) return Boolean(item.disabled(parent || item))
  return Boolean(item.disabled)
}

support.js 支持

定义公共方法,按需导入。

/**
 * FPC 是 Function Param Console 的简称
 * 方法参数控制
 */

export function FPC() {
  /**
   * 错误参数提示
   */
  this.error = (param, text, accepts) => {
    return this.matching({
      level: 'error',
      param: param,
      text: text,
      accepts: accepts
    })
  }
  /**
   * 警告参数提示,是 warning 的简写
   */
  this.warn = (param, text, accepts) => {
    return this.matching({
      level: 'warn',
      param: param,
      text: text,
      accepts: accepts
    })
  }

  /**
   * 废弃参数提示,是 obsolete 的简写
   */
  this.obs = (param, text, accepts) => {
    return this.matching({
      level: 'error',
      param: param,
      text: text,
      accepts: accepts
    })
  }

  /**
   * 匹配参数提示
   * @params { level = 'error', param = '', text, accepts = '*' }
   */
  this.matching = (params) => {
    const { level = 'error', param = '', text, accepts = '*' } = params // 参数对象解构
    const T_TYPE = getDataType(text) // 文本的数据类型
    if (accepts !== '*' && !accepts.includes(T_TYPE)) {
      console[level](`${param}:数据类型不匹配,期望接收[${accepts.join('、')}]参数,意外的获得“${T_TYPE}”参数!`)
      return {
        dataType: T_TYPE,
        result: false
      }
    }
    return {
      dataType: T_TYPE,
      result: true
    }
  }
}

/**
 * 获得参数的数据类型
 * @param text 目标文本
 * @returns {undefined|null|string}
 */
export function getDataType(text) {
  const textType = typeof text
  switch (textType) {
    case 'undefined':
      return undefined
    case 'function':
      return 'function'
    case 'number':
      return 'number'
    case 'object':
      if (text === null) return null
      if (text.constructor.name === 'Array') return 'array'
      return 'object'
    case 'boolean':
      return 'boolean'
    case 'string':
      return 'string'
    case 'bigint':
      return 'bigint'
    case 'symbol':
      return 'symbol'
  }
}

/**
 * 可接收的数据类型
 * @param text 目标文本
 * @param dataTypes 可接受的数据类型列表
 * @returns {void, boolean}
 */
export function acceptDataTypes(text, dataTypes) {
  const A_TYPES = getDataType(dataTypes)
  if (text === undefined) return console.error(`text:期望接收一个非“undefined”参数,意外的获得一个“undefined”参数!`)
  if (A_TYPES !== 'array') return console.error(`dataTypes:期望接收一个“array”参数,意外的获得一个“${A_TYPES}”参数!`)
  return A_TYPES.includes(getDataType(text))
}

/**
 * 对象多层级取值,避免报错
 * @param data 取值的对象
 * @param path 取值的路径
 * @returns {undefined}
 * @private
 */
export function _get(data, path) {
  if (isVoid(data)) return undefined
  if (_typeof(path) !== String) return undefined
  const pathArr = path.split('.')
  let result = data
  for (let i = 0; i < pathArr.length; i++) {
    if (result[pathArr[i]] === undefined) return undefined
    result = result[pathArr[i]]
  }
  return result
}

/**
 * 获得参数的数据类型
 * @param text 目标文本
 * @returns {undefined|null|string|Function|Number|Array|Object|Boolean|BigInt|Symbol}
 */
export function _typeof(text) {
  const textType = typeof text
  switch (textType) {
    case 'undefined':
      return undefined
    case 'function':
      return Function
    case 'number':
      return Number
    case 'object':
      if (text === null) return null
      if (Array.isArray(text)) return Array
      return Object
    case 'boolean':
      return Boolean
    case 'string':
      return String
    case 'bigint':
      // eslint-disable-next-line no-undef
      return BigInt
    case 'symbol':
      return Symbol
  }
}

/**
 * 校验数据类型
 * @param text 目标文本
 * @param type 预期类型
 * @returns {void, boolean}
 */
export function _typein(text, type) {
  const TYPE = _typeof(type)
  const TEXT = _typeof(text)
  if (TYPE === Array) return type.includes(TEXT)
  return type === TEXT
}

/**
 * 判断文本是否无效
 * @param text 待检测的文本
 * @param includeZero 是否包括零,默认false
 * @returns {boolean}
 */
export function isVoid(text, includeZero = false) {
  if (text === null || text === undefined || text === '') return true
  return includeZero && (text === 0)
}

/**
 * 数据是否有效
 * @param text 目标文本
 * @returns {boolean}
 */
export function isValid(text) {
  return !isVoid(text)
}

/**
 * 输入限制/只能输入数字且不能以0开头
 * @param val 输入值
 * @returns {Number}
 */
export function limitInputNumber(val) {
  return val.replace(/\D/g, '').replace(/^0{1,}/g, '')
}

/**
 * 友好展示文本信息
 * @param text 待展示的文本
 * @param displace 当文本无效时,取代展示的文本
 * @param other 当文本有效时,要求展示其他文本
 * @param added 补充显示的条件,默认值true
 * @returns {string|*}
 */
export function friendly(text, displace = '', added = true, other) {
  if (isVoid(text) && added) return displace
  return other !== undefined ? other : text
}

/**
 * 防止无效的输入
 * 去掉字符串左边的空格,连续的空格替换为一个空格
 * @param params
 * -params.text 需要去除多余空格的文本,默认值“”
 * -params.trimStart 是否需要去除文本左边的空格,默认值true
 * -params.trimEnd 是否需要去除文本右边的空格,默认值false
 * -params.trimAll 是否需要去除文本所有的空格,默认值false
 * @returns {*}
 */
export function preventVoidInput(params) {
  const dataType = getDataType(params)
  if (dataType === 'Sting') {
    params = { text: params }
  } else if (dataType !== 'object') {
    return console.error(`params: 期望接收一个“object”参数,意外的获得一个“${dataType}”参数!`)
  }
  const { trimStart = true, trimEnd = false, trimAll = false } = params // 参数对象解构
  let { text = '' } = params // 参数对象解构
  if (isVoid(text)) return ''
  if (trimAll) return text.replace(/\s+/g, '')
  if (trimStart) {
    text = text.trimStart()
  }
  if (trimEnd) return text.trimEnd()
  return text.replace(/\s+/g, ' ')
}

/**
 * 获取数组对象的值,返回一个值的数组
 * @param list 数组对象
 * @param func 需要获取的值,处理方法
 * @returns {void|*}
 */
export function getObjectValues(list, func) {
  const LIST_DATA_TYPE = getDataType(list) // 数据类型
  if (LIST_DATA_TYPE !== 'array') return console.error(`list: 期望接收一个“array”参数,意外的获得一个“${LIST_DATA_TYPE}”参数!`)
  const FUNC_DATA_TYPE = getDataType(func) // 数据类型
  if (FUNC_DATA_TYPE !== 'function') return console.error(`func: 期望接收一个“function”参数,意外的获得一个“${FUNC_DATA_TYPE}”参数!`)
  return list.map(obj => func(obj))
}

/**
 * 参数填充
 * 接口设计采用 Restfull API 需要遵循的规则,动参一律采用$符号定义。
 * 具名参数: $符号后面带参数
 * 匿名参数: $符号后面不带参数
 * 具名参数示例1: const api1 = '/api/$mode/$id/evaluations/$eid/'
 * paramsPadding(api1, { $mode: 'course', $id: 1, $eid: 2 })
 * 匿名参数示例2: const api2 = '/api/$/$/evaluations/$/'
 * paramsPadding(api2, ['course', 1, 2])
 * @param text 待填充接口
 * @param params 参数对象
 * @returns {*}
 */
export function paramsPadding(text, params) {
  const fpc = new FPC()
  const TEXT = fpc.error('text', text, ['string'])
  if (!TEXT.result) return
  const PARAMS = fpc.error('params', params, ['object', 'array'])
  if (!PARAMS.result) return
  if (PARAMS.dataType === 'object') {
    Object.keys(params).map(key => {
      text = text.replace(key, params[key])
    })
    return text
  }
  params.map(value => {
    text = text.replace('$', value)
  })
  return text
}

/**
 * 文本替换
 * @param str 原始字符串
 * @param index 指定下标
 * @param oldText 旧文本
 * @param newText 新文本
 * @returns {string} 新的字符串
 */
export function textReplace(str, index, oldText, newText) {
  return str.substring(0, index) + newText + str.substring(index + oldText.length)
}

/**
 * 寻找子字符串在父字符串中所有出现的下标
 * @param supStr 父字符串
 * @param subStr 子字符串
 * @returns {*[]} 数组
 */
export function findSubStrIndex(supStr, subStr) {
  let index = supStr.indexOf(subStr)
  const result = []
  while (index > -1) {
    result.push(index)
    index = supStr.indexOf(subStr, index + 1)
  }
  return result
}

/**
 * 解析 Blob文件内容
 * @param blob
 * @returns {Promise<unknown>}
 */
export function parseBlob(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = function(res) {
      resolve(JSON.parse(res.target.result))
    } // 成功回调
    reader.onerror = function(err) {
      reject(err)
    } // 失败回调
    reader.readAsText(blob, 'utf-8') // 按照utf-8编码解析
  })
}

/**
 * 深度克隆对象
 * 适用范围:
 * JSON.stringify()只能序列化对象的可枚举的自有属性,obj中的对象由构造函数生成,使用方法后该对象__proto__不再为其构造函数的prototype,失去与其构造函数的链接,不可枚举的属性丢失
 * 以下场景不适用:
 * object包含:Date对象、RegExp对象、Map对象、Set对象、Error对象、包装对象(new Number()、new String()、new Boolean())、Function对象、Symbol、BigInt、undefined、NaN、Infinity、-Infinity
 * object中存在循环引用的情况也无法正确实现深拷贝,会引发 TypeError(TypeError: Converting circular structure to JSON)
 * @param object 目标对象
 */
export function deepClone(object) {
  return JSON.parse(JSON.stringify(object))
}

/**
 * 下载文件
 * @param fileSource blob文件格式或者下载地址URL
 * @param fileName 是文件的别名是一个可选参数
 *
 */
export function downloadFile(fileSource, fileName) {
  if (isVoid(fileSource)) return false
  const fileSourceType = _typeof(fileSource)
  if (fileSourceType === String && isValid(fileSource)) return downloadFileAsHref(fileSource, fileName)
  if (fileSourceType === Object && fileSource.constructor.name === 'Blob') {
    if (window.navigator['msSaveOrOpenBlob']) {
      navigator['msSaveBlob'](fileSource, fileName)
      return true
    } else {
      const url = window.URL ? window.URL.createObjectURL(fileSource) : window.webkitURL.createObjectURL(fileSource)
      downloadFileAsHref(url, fileName)
      window.URL ? window.URL.revokeObjectURL(url) : window.webkitURL.revokeObjectURL(url)
      return true
    }
  }
  return false
}

/**
 * 下载文件-通过URL
 * @param fileUrl,是文件的下载地址
 * @param fileName,是文件的别名是一个可选参数
 * return boolean,true 已执行下载请求,false fileUrl是空的,未执行下载请求
 */
export function downloadFileAsHref(fileUrl, fileName) {
  if (!fileUrl) return false
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = fileUrl
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  return true
}

/**
 * 计算表达式的值, 解决eval表达式报错的问题
 * @param fn
 * @returns {*}
 */
export function evil(fn) {
  const Fun = Function // 一个变量指向Function,防止有些前端编译工具报错
  return new Fun('return ' + fn)()
}

/**
 * 转换树结构
 * @param treeData 一维数组或者一维对象
 * @param id 节点id
 * @param pId 节点的父id
 * @param rootPId 根节点id(一般为0)
 * @returns {*[]}
 */
export function transTree(treeData, { id, pId, rootPId }) {
  const keyNodes = {}
  const rootNodeList = []
  const nodeList = []
  if (_typeof(treeData) === Array) {
    // Fill in the map
    treeData.map(node => {
      const clone = { ...node }
      const key = clone[id]
      keyNodes[key] = clone
      clone.key = clone.key || key
      nodeList.push(clone)
    })
  } else if (_typeof(treeData) === Object) {
    // Fill in the map
    Object.keys(treeData).map(index => {
      const node = treeData[index]
      const clone = { ...node }
      const key = clone[id]
      keyNodes[key] = clone
      clone.key = clone.key || key
      nodeList.push(clone)
    })
  }

  // Connect tree
  nodeList.forEach(node => {
    const parentKey = node[pId]
    const parent = keyNodes[parentKey]

    // Fill parent
    if (parent) {
      parent.children = parent.children || []
      parent.children.push(node)
    }

    // Fill root tree node
    if (parentKey === rootPId || (!parent && rootPId === null)) {
      rootNodeList.push(node)
    }
  })

  return rootNodeList
}

/**
 * 文本域 Enter 分割转数组
 * @param str
 * @returns {string[]|*[]}
 */
export function textareaToArray(str) {
  if (typeof str === 'string') {
    return str.split(/[(\r\n)\r\n]+/)
  } else {
    return []
  }
}

/**
 * 解析JSON格式的数组
 * 特殊符号,如英文逗号隔开的也可以转成数组
 * @param text 需要解析的文本对象
 * @param symbol 特殊间隔符号
 * @returns {*[]|*}
 */
export function parseJsonArray(text, symbol = ',') {
  try {
    if (isVoid(text)) return []
    if (_typeof(text) === Array) return text
    const original = evil(text)
    if (_typeof(original) === Array) return original
    return [original]
  } catch (e) {
    if (text.includes(symbol)) return text.split(symbol)
    return [text]
  }
}

/**
 * 过滤HTML标签
 * @param text 需要过滤的文本
 * @returns {string} 过滤后的文本
 */
export function filterHtmlTag(text) {
  return text.replace(/</g, '&lt;').replace(/</g, '&gt;')
}

/**
 * 回车符替换为<br>标签
 * @param text
 * @returns {string}
 */
export function replaceEnterSymbol(text) {
  return text.replace(/\r\n/g, '<br>').replace(/\/r\/n/g, '<br>').replace(/\/n/g, '<br>').replace(/\n/g, '<br>')
}

/**
 * 是否是IE浏览器
 * @param version
 * @returns {boolean}
 */
export function isIE(version) {
  function isAnyIeVersion() {
    const agent = navigator.userAgent.toLowerCase()
    return agent.indexOf('msie') !== -1 || agent.indexOf('trident') !== -1 || agent.indexOf(' edge/') !== -1
  }

  if (!isAnyIeVersion()) {
    return false
  }
  if (!version) {
    return true
  }
  // Shamelessly stolen from https://gist.github.com/padolsey/527683
  const ieVersion = (function() {
    let v = 3
    const div = document.createElement('div')
    const all = div.getElementsByTagName('i')
    do {
      div.innerHTML = '<!--[if gt IE ' + ++v + ']><i></i><![endif]-->'
    } while (all[0])
    return v > 4 ? v : void (0)
  })()
  return version === ieVersion
}

/**
 * 获取滚动条的宽度
 * @returns {number}
 */
export function getScrollWidth() {
  // 创建一个div元素
  const oDiv = document.createElement('div')
  oDiv.style.cssText = 'position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;'
  const noScroll = document.body.appendChild(oDiv).clientWidth
  // 没有滚动条的clientWidth为content+paddingLeft+paddingRight不包括滚动条,可以利用这个获取差值来求滚动条的宽度
  oDiv.style.overflowY = 'scroll'
  const scroll = oDiv.clientWidth
  document.body.removeChild(oDiv)
  return noScroll - scroll
}

/**
 * 节流函数,表示一段时间内只执行一次
 * 使用方法: throttle(func, 200)
 * @param fn 需要执行方法
 * @param {number} delay 毫秒,节流期限值
 * @param {boolean} immediate 是否立即执行一次, 再等待节流
 */
let flagThrottle = true

export function throttle(fn, delay = 200, immediate = true) {
  return (function() {
    if (!flagThrottle) return
    flagThrottle = false
    const args = arguments
    if (immediate) {
      fn.apply(this, args) // 立即执行
    }
    setTimeout(() => {
      if (!immediate) {
        fn.apply(this, args) // 非立即执行
      }
      flagThrottle = true
    }, delay)
  }())
}

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

推荐阅读更多精彩内容