可视化工具入门介绍(一)

  • 难点:地图可视化
  • 地图常见用法:控件,绘图,动画
  • 地图可视化:散点
  • 数据可视化,是关于数据视觉表现形式的科学技术研究

就是将数据转换成易于人员辨识和理解的数据形式,比如2D,3D视图等,底层是计算机图形学

3.1前端数据可视化解决方案如下

  1. highcharts
  2. Echarts
  3. AntV
  4. three.js
  5. zrender
  6. d3
  7. canvas
  8. Svg
  9. WebGL
  10. HTML
  11. Chorme(Skia,OPenGL)

Skia:是chorme和Android的底层2D绘图引擎
OPenGL:2D,3D图形渲染库

1.Canvas简单介绍

  • 编写canvas标签(注意指定宽高)
  • 获取canvas DOM 对象
  • 获取canvas 对象
  • 设置绘图属性
  • 调用绘图API
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Canvas</title>
  </head>
  <body>
    <canvas id="canvas" width="800" height="800"></canvas>
    <script>
      const canvas = document.getElementById('canvas');
      const ctx = canvas.getContext('2d'); // 获取Canvas 对象
      ctx.fillStyle = 'red'; // 修改填充色为红色
      ctx.fillRect(0, 0, 50, 50); // 绘制矩形,0,0 XY轴坐标 50,50是宽高

      // 绘制线条
      ctx.beginPath();
      ctx.lineWidth = 1; // 线条宽度
      ctx.strokeStyle = 'blue'; // 线条填充色
      ctx.moveTo(100, 100); // 起点坐标
      ctx.lineTo(250, 75); // 中间点坐标
      ctx.lineTo(400, 140); // 终点坐标
      ctx.stroke(); // 绘制线段

      // 绘制圆型
      ctx.beginPath();
      ctx.lineWidth = 3; // 圆形宽度
      ctx.strokeStyle = 'green'; // 圆形填充色
      ctx.fillStyle = 'red';
      ctx.arc(300, 300, 50, 0, 2 * Math.PI); // 绘制圆形,300,300 圆心坐标 50半径
      ctx.stroke(); // 绘制圆边框
      ctx.fill(); // 填充圆

      // 绘制点
      ctx.beginPath();
      ctx.lineWidth = 2; // 线条宽度
      ctx.strokeStyle = 'red'; // 线条填充色
      ctx.moveTo(400, 400); // 起点坐标
      ctx.lineTo(401, 401); // 终点坐标
      ctx.stroke(); // 绘制线段
    </script>
  </body>
</html>
  • 最终效果
image1.png

Canvas进阶案列:图片压缩

  • 使用场景:前端允许上传3M的图片
  1. 将图片文件转成base64
// 将图片转化成base64
    function converImageToBase64(file, callback) {
      // 实例化fileReader对象
      let reader = new FileReader();
      // 文件加载完成之后
      reader.addEventListener('load', function (e) {
        const base64Image = e.target.result;
        callback && callback(base64Image);
        // 完成之后,让内存去回收 reader
        reader = null;
      });
      reader.readAsDataURL(file);
    }

2.计算压缩比,通过最大高度,最大宽度进行计算

3.使用canvas 重新绘制(压缩尺寸后的)

  1. 进行分辨率压缩,使用canvas.toDataURL进行二次压缩
 // 图片压缩函数 
// 思路:1,图片尺寸压缩,获取图片宽高 2,图片输出分辨率
    function compressImage(base64Image, callback) {
      let maxW = 1024;
      let maxH = 1024;
      // 渲染图片
      const image = new Image();
      // 对图片宽高进行压缩
      image.addEventListener('load', function (e) {
        let ratio; // 图片压缩比
        let needCompress = false; // 是否需要压缩
        // 获取图片的原始宽高  image.naturalWidth, image.naturalHeight
        // 先判断图片宽度是否要压缩
        if (maxW < image.naturalWidth) {
          needCompress = true;
          // 计算压缩比
          ratio = image.naturalWidth / maxW;
          console.log(ratio, 'ratio'); // 1.25
          // 图片高度也是要同比压缩,经过压缩之后的图片宽高比例不变
          maxH = image.naturalHeight / ratio;
          // 经过压缩之后的图片尺寸为 1024 * 724
          console.log(image.naturalWidth, image.naturalHeight, maxH, 'maxH'); // 1280 905 704
        }
        // 判断图片高度是否要压缩
        if (maxH < image.naturalHeight) {
          needCompress = true;
          ratio = image.naturalHeight / maxH;
          maxW = image.naturalWidth / ratio;
        }
        // 不需要压缩,获取图片的实际尺寸
        if (!needCompress) {
          maxW = image.naturalWidth;
          maxH = image.naturalHeight;
        }
        // 最后使用canvas 重新绘制压缩图片

        const canvas = document.createElement('canvas');
        canvas.setAttribute('id', '_compress_');
        canvas.width = maxW;
        canvas.height = maxH;
        // 压缩效果,实际过程中,隐藏压缩过程,设置为hidden
        canvas.style.visibility = 'hidden';
        document.body.appendChild(canvas);

        // 进行绘图
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, maxW, maxH);
        // 绘制图片
        ctx.drawImage(image, 0, 0, maxW, maxH);
        // 将图片转换为base64,第一个是参数是图片类型,第二个,压缩参数 0-1,一般设置0.8,0.9
        const compressImage = canvas.toDataURL('image/jpg', 0.1);
        // 将压缩后的图片保存
        callback && callback(compressImage);
        canvas.remove();
        // // 展示对比效果
        // const imageCompressNode = new Image();
        // imageCompressNode.src = compressImage;
        // console.log(imageCompressNode.size);
        // document.body.appendChild(imageCompressNode);
      });
      // image.src = base64Image;
      // document.body.appendChild(image);
    }
  1. 将图片保存并上传到服务器(传入文件及回调函数)
// 上传函数方法
    function uploadToServer(compressImage) {
      // 将压缩后的图片上传到服务器
      console.log(compressImage, 'compressImage');
    }
  • 最终代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>图片压缩</title>
  </head>
  <body>
    <input type="file" id="upload" />
  </body>
  <script>
    // 可接受文件类型
    const ACCEPT = ['image/jpg', 'image/png', 'image/jpeg'];
    const MaxSize = 1024 * 1024;
    const upload = document.getElementById('upload');
    // 将图片转化成base64
    function converImageToBase64(file, callback) {
      // 实例化fileReader对象
      let reader = new FileReader();
      // 文件加载完成之后
      reader.addEventListener('load', function (e) {
        const base64Image = e.target.result;
        callback && callback(base64Image);
        // 完成之后,让内存去回收 reader
        reader = null;
      });
      reader.readAsDataURL(file);
    }
    // 上传函数方法
    function uploadToServer(compressImage) {
      // 将压缩后的图片上传到服务器
      console.log(compressImage, 'compressImage');
    }
    // 图片压缩函数 思路:1,图片尺寸压缩,获取图片宽高 2,图片输出分辨率
    function compressImage(base64Image, callback) {
      let maxW = 1024;
      let maxH = 1024;
      // 渲染图片
      const image = new Image();
      // 对图片宽高进行压缩
      image.addEventListener('load', function (e) {
        let ratio; // 图片压缩比
        let needCompress = false; // 是否需要压缩
        // 获取图片的原始宽高  image.naturalWidth, image.naturalHeight
        // 先判断图片宽度是否要压缩
        if (maxW < image.naturalWidth) {
          needCompress = true;
          // 计算压缩比
          ratio = image.naturalWidth / maxW;
          console.log(ratio, 'ratio'); // 1.25
          // 图片高度也是要同比压缩,经过压缩之后的图片宽高比例不变
          maxH = image.naturalHeight / ratio;
          // 经过压缩之后的图片尺寸为 1024 * 724
          console.log(image.naturalWidth, image.naturalHeight, maxH, 'maxH'); // 1280 905 704
        }
        // 判断图片高度是否要压缩
        if (maxH < image.naturalHeight) {
          needCompress = true;
          ratio = image.naturalHeight / maxH;
          maxW = image.naturalWidth / ratio;
        }
        // 不需要压缩,获取图片的实际尺寸
        if (!needCompress) {
          maxW = image.naturalWidth;
          maxH = image.naturalHeight;
        }
        // 最后使用canvas 重新绘制压缩图片

        const canvas = document.createElement('canvas');
        canvas.setAttribute('id', '_compress_');
        canvas.width = maxW;
        canvas.height = maxH;
        // 压缩效果,实际过程中,隐藏压缩过程,设置为hidden
        canvas.style.visibility = 'hidden';
        document.body.appendChild(canvas);

        // 进行绘图
        const ctx = canvas.getContext('2d');
        ctx.clearRect(0, 0, maxW, maxH);
        // 绘制图片
        ctx.drawImage(image, 0, 0, maxW, maxH);
        // 将图片转换为base64,第一个是参数是图片类型,第二个,压缩参数 0-1,一般设置0.8,0.9
        const compressImage = canvas.toDataURL('image/jpg', 0.1);
        // 将压缩后的图片保存
        callback && callback(compressImage);
        canvas.remove();
        // // 展示对比效果
        // const imageCompressNode = new Image();
        // imageCompressNode.src = compressImage;
        // console.log(imageCompressNode.size);
        // document.body.appendChild(imageCompressNode);
      });
      // image.src = base64Image;
      // document.body.appendChild(image);
    }
    upload.addEventListener('change', function (e) {
      // 数组解构写法
      // 等价于 const file = e.target.files[0]
      const [file] = e.target.files;
      if (!file) {
        return;
      }
      const { type: fileType, size: fileSize } = file;
      // 判断文件类型是否包含在里面
      if (!ACCEPT.includes(fileType)) {
        console.log(`不支持${fileType}文件类型`);
        upload.value = '';
        return;
      }
      // 图片容量检查
      if (fileSize > MaxSize) {
        console.log(`文件超出${MaxSize}`);
        upload.value = '';
        return;
      }
      // 压缩图片
      /*
       * 1,将文件转成base64
         2,计算压缩比,通过最大高度,最大宽度进行计算
         3,使用canvas 重新绘制(压缩尺寸后的)
         4,进行分辨率压缩,使用canvas.toDataURL进行二次压缩
       * 5,将图片保存并上传到服务器(传入文件及回调函数)
       */
      converImageToBase64(file, base64Image =>
        compressImage(base64Image, uploadToServer)
      );
    });
  </script>
</html>

2.svg入门介绍

SVG是一种基于XML的图形文件格式,英文全称为Scalable Vector Graphics,意思为可缩放的矢量图形

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SVG</title>
  </head>
  <body>
    <svg width="800" height="800">
      <rect width="50" height="50" style="fill: red"></rect>
      <line
        x1="100"
        y1="100"
        x2="250"
        y2="75"
        style="stroke: blue; stroke-width: 1"
      ></line>
      <line
        x1="250"
        y1="75"
        x2="450"
        y2="175"
        style="stroke: blue; stroke-width: 1"
      ></line>
      <circle
        cx="200"
        cy="200"
        r="50"
        stroke="green"
        stroke-width="2"
        fill="red"
      ></circle>
           <line
        x1="300"
        y1="300"
        x2="301"
        y2="301"
        style="stroke: blue; stroke-width: 1"
      ></line>
    </svg>
  </body>
</html>
  • 最终效果图


    image2.png

3.webgl技术分享

WebGL(web Graphics Library) 是一种3D绘图协议,WebGL可以为HTML Canvas 提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来再浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。

4.zrender入门学习

zrender 是二维绘图引擎,它提供Canvas,SVG,VML等多种渲染方式。zrender也是Echarts的渲染器。

  • 引入zrender库
  • 编写div容器
  • 初始化zrender对象
  • 初始化zrender绘图对象
  • 调用zrender add 方法绘图
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>zrender</title>
    <script src="https://cdn.jsdelivr.net/npm/zrender@4.3.0/dist/zrender.js"></script>
  </head>
  <body>
    <div id="container" style="width: 800px; height: 800px"></div>
    <script>
      const zr = zrender.init(document.getElementById('container'));
      // 绘制矩形
      const rect = new zrender.Rect({
        shape: {
          x: 0,
          y: 0,
          width: 50,
          height: 50,
        },
        style: {
          fill: 'red',
          lineWidth: 0,
        },
      });
      /* 绘制一条线 */
      var line = new zrender.Polyline({
        shape: {
          points: [
            [100, 100],
            [300, 200],
            [150, 500],
          ],
        },
        style: {
          stroke: '#0000ff',
          lineWidth: 3,
        },
      });
      /* 绘制点 */
      var pot = new zrender.Polyline({
        shape: {
          points: [
            [300, 300],
            [301, 301],
          ],
        },
        style: {
          stroke: 'red',
          lineWidth: 1,
        },
      });

      /* 绘制一个圆 */
      var circle = new zrender.Circle({
        shape: {
          cx: 366,
          cy: 366,
          r: 50,
        },
        style: {
          fill: '#0fa400',
          stroke: '#eeaa45',
          lineWidth: 2,
        },
      });

      zr.add(rect);
      zr.add(line);
      zr.add(pot);
      zr.add(circle);
    </script>
  </body>
</html>
  • 最终实现效果图


    image3.png

5.d3入门介绍

D3 (Data-Driven Documents)是一个Javascript图形库,基于Canvas,Svg和HTML.

https://d3js.org/
// 入门学习链接
https://zhuanlan.zhihu.com/p/38001672
  • 数据绑定案列
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>D3</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>
  </head>
  <body>
    <p>vue</p>
    <p>react</p>
    <p>agular</p>
    <button id="datum">datum</button>
    <!-- 对整体数据源进行更新 -->
    <button id="data">data</button>
  </body>
  <script>
    // 这个body 是d3里面的document,并不是浏览器里面的document
    const body = d3.select('body');
    // 获取body 所有的p标签
    const p = body.selectAll('p');
    function doDatum() {
      const str = 'Violet';
      p.datum(str);
      p.text(function (d, i) {
        console.log(d, i);
        return `${d}-${i}`;
      });
    }
    function doData() {
      const dataSet = ['Vue', 'React', 'Agular'];
      p.data(dataSet).text(function (d, i) {
        return `${d}-${i}`;
      });
    }
    document.getElementById('datum').addEventListener('click', function (e) {
      doDatum();
    });
    document.getElementById('data').addEventListener('click', function (e) {
      doData();
    });
  </script>
</html>
  • 结果如下


    image4.png
image5.png

6.Three.js入门介绍

Three.js是一个基于WebGL的javascript 3D图形库

  • 案列:旋转正方体

7.highcharts入门介绍

Highcharts 是一个用纯javaScript编写的图表库,能够很简单便捷的在web网站或是web应用程序添加有交互性的图表,它包括Highcharts JS,Highstock JS,Highmaps JS共三款软件,均为纯js编写的HTML5图库。

https://www.highcharts.com.cn/demo/highcharts

8.antv入门介绍

Antv 是蚂蚁金服全新一代数据可视化方案,致力于提供一套简单方便,专业可靠,无限可能的数据可视化最佳实践。

  1. 它包括以下解决方案:
  • G2:可视化引擎
  • G2Plot:图表库
  • G6:图可视化引擎-思维导图,关系图等
  • Graphin:基于G6的图分析组件
  • F2:移动可视化方案
  • ChartCube:AntV图表在线制作
  • L7:地理空间数据可视化
https://www.highcharts.com.cn/demo/highcharts
8.1 antv-g2入门
https://antv-g2.gitee.io/zh/examples/gallery
  • 实现代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>g2实现折线图</title>
    <script src="https://gw.alipayobjects.com/os/lib/antv/g2/4.1.16/dist/g2.min.js"></script>
  </head>
  <body>
    <div id="container"></div>
  </body>
  <script>
    // 初始化
    // 内容
    const data = [
      { year: '1991', value: 3 },
      { year: '1992', value: 4 },
      { year: '1993', value: 3.5 },
      { year: '1994', value: 5 },
      { year: '1995', value: 4.9 },
      { year: '1996', value: 6 },
      { year: '1997', value: 7 },
      { year: '1998', value: 9 },
      { year: '1999', value: 13 },
    ];
    // 创建G2图表示例化
    const chart = new G2.Chart({
      container: 'container',
      autoFit: true,
      height: 500,
    });

    chart.data(data);
    chart.axis('value', {
      label: {
        formatter: val => {
          return +val + 'k';
        },
      },
    });
    chart.scale({
      year: {
        range: [0, 1],
      },
      value: {
        min: 0,
        nice: true,
      },
    });
    chart.tooltip({
      showCrosshairs: true, // 展示 Tooltip 辅助线
      shared: true,
    });
    chart.line().position('year*value').label('value').color('#ff5957');
    chart.point().position('year*value').color('#ff5957');
    chart.render();
  </script>
</html>
  • 最终结果


    image6.png
8.2 antv-g6入门
  • 节点demo
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>g6图表demo</title>
    <script src="https://gw.alipayobjects.com/os/lib/antv/g6/3.7.1/dist/g6.min.js"></script>
  </head>
  <body>
    <div id="mountNode"></div>
     <script>
      // 定义数据源
      const data = {
        // 点集
        nodes: [
          {
            id: 'node1',
            label: '开始',
            x: 100,
            y: 200,
            size: 80,
            labelCfg: {
              position: 'center',
              style: {
                fontSize: 16,
                fill: 'red',
              },
            },
          },
          {
            id: 'node2',
            label: '连接',
            x: 300,
            y: 200,
          },
          {
            id: 'node3',
            label: '结束',
            x: 500,
            y: 200,
          },
          {
            id: 'node4',
            label: '结束4',
            x: 300,
            y: 400,
          },
        ],
        // 边集
        edges: [
          // 表示一条从 node1 节点连接到 node2 节点的边
          {
            source: 'node1',
            target: 'node2',
            label: '连接线1',
          },
          {
            source: 'node2',
            target: 'node3',
            label: '连接线2',
          },
          {
            source: 'node1',
            target: 'node4',
            label: '连接线3',
          },
        ],
      };
      // 创建
      const graph = new G6.Graph({
        container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
        // 画布宽高
        width: 800,
        height: 500,
        defaultNode: {
          shape: 'circle',
          size: [100],
          color: '#5B8FF9',
          style: {
            fill: '#9EC9FF',
            lineWidth: 3,
          },
          labelCfg: {
            style: {
              fill: 'yellow',
              fontSize: 20,
            },
          },
        },
        defaultEdge: {
          style: {
            stroke: '#e2e2e2',
          },
        },
      });
      // 读取数据
      graph.data(data);
      // 渲染图
      graph.render();
    </script>
  </body>
</html>
  • 最终结果


    image7.png
8.3 antv-L7入门
  • 绘制气泡地图
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>L7demo</title>
    <script src="https://unpkg.com/@antv/l7"></script>
  </head>
  <body>
    <div id="map"></div>
  </body>
  <script>
    const scene = new L7.Scene({
      id: 'map',
      map: new L7.GaodeMap({
        pitch: 0, // 角度
        style: 'white',
        center: [96.99215001469588, 29.281597225674773],
        zoom: 12,
        maxZoom: 10,
      }),
    });
    // 地图绘制完成,即scence 加载完成,在绘制点
    scene.on('loaded', () => {
      fetch(
        'https://gw.alipayobjects.com/os/basement_prod/337ddbb7-aa3f-4679-ab60-d64359241955.json'
      )
        .then(res => res.json())
        .then(data => {
          data.features = data.features.filter(item => {
            return item.properties.capacity > 800;
          });
          // 气泡图通过 PointLayer 对象实例化,
          const pointLayer = new L7.PointLayer({})
            .source(data)
            .shape('circle')
            .size('capacity', [0, 16])
            .color('capacity', [
              '#34B6B7',
              '#4AC5AF',
              '#5FD3A6',
              '#7BE39E',
              '#A1EDB8',
              '#CEF8D6',
            ])
            .active(true)
            .style({
              opacity: 0.5,
              strokeWidth: 0,
            });

          scene.addLayer(pointLayer);
        });
    });
  </script>
</html>
  • 最终结果


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

推荐阅读更多精彩内容