#5 从零开始制作在线 代码编辑器

上一篇
#4 从零开始制作在线 代码编辑器

删除 与 BackSpace 与 Delete


BackSpace

为了方便. 这里所说的删除的只考虑以下两种键的最简单的删除行为:

  1. BackSpace 往左边删除一个字符
  2. Delete 往右边删除一个字符

有选区状态时所做的删除或者替换. 这里不考虑哦.

因为做了简化. 这里的流程就会比较简单. 要说明的是:
当光标位于行首时. 再使用BackSpace 的时候. 要删除当前行.
把这个方法加在 harusame-line.js

code

@path serval/script/harusame-line.js
// 只有部分代码

/**
 * 删除指定行
 */
self.deleteLine = function (v_line_number) {
    var $line_container = document.querySelector('.line-container') // 同样. 这里暂时这么写...
    var $line = self.getLineContentByLogicalY(v_line_number).parentNode.parentNode
    $line_container.removeChild($line)
}
@path serval/script/harusame-serval.js
// 只有部分代码
  
/**
 * KEY: BackSpace  
 * 0. 阻止默认行为
 * 1. 如果光标在首行首列, 什么都不干
 * 2. 如果光标在该行第0个位置
 *      2.1. 得到光标后的内容(left_content), 删除当前行
 *      2.2. 修正行号 -> 暂时是这样
 *      2.3. 光标上移一行, 且放置到该行最后一列
 *      2.4. 将上一行的残留下来的内容(left_content) 追加到该行末尾
 * 3. 其他情况下
 *      3.1. 光标往左移动一列
 *      3.2. 删除一个字符
 */
Serval.prototype.keydownHandler.'8': function (event) {
    event.preventDefault() /* 0 */
    var self = this
    self.allocTask(function (v_cursor) {
        var $line = v_cursor.line.$line_content
        var textContent = $line.textContent
        var logicalX = v_cursor.logicalX
        var left_content = textContent.substring(logicalX, textContent.length)

        var logicalY = v_cursor.logicalY
        if (logicalY === 0) {
            if (logicalX === 0) {
                return /* 2 */
            }
        } else {
            if (logicalX === 0) { /* 2 */
                Line.deleteLine(v_cursor.logicalY) /* 2.1 */
                Line.fixLineNumber(v_cursor.logicalY) /* 2.2 */

                v_cursor.logicalY -= 1 /* 2.3 */
                v_cursor.logicalX = v_cursor.line.$line_content.textContent /* 2.3 */

                var $line = v_cursor.line.$line_content
                var textContent = $line.textContent
                $line.textContent = textContent + left_content /* 2.4 */

                return
            }
        }

        v_cursor.logicalX -= 1 /* 3.1 */
        $line.textContent = textContent.substring(0, logicalX - 1) + left_content /* 3.2 */
    })
},

效果是这样... 很普通. 见 图5-1

图5-1.gif

Delete

Delete 的原理同 BackSpace. 但是还是有一些差异. 在做的时候. 能感受到之前代码的不合理(蠢).

Delete 中. 当光标位于 最后一行 最后一列 时. 需要先得到一共有几行. 才能做比较. 可是问题在于访问 Line.max_line_number 会触发它的 getter. 并且修改了数据. 见 图5-2. 这是一个看似简单但是会充满麻烦的行为. 毕竟每次访问. 他都在变. 导致在做 最后一行 最后一列 的判断时. 只要按下 一次以上的 Delete. 这个判断就会失效.

图5-2.png

先把问题补了吧...

  1. ++ 改成 删掉 (这里暂时这么改... 事实上没有必要控制 getter setter 了)
  1. 每次调用 Line.generateLine 的时候. 最大行数加一.
  1. 同样地. 每次调用Line.deleteLine的时候. 最大行数减一.

code

@path serval/script/harusame-serval.js
// 部分代码

/**
 * KEY: Delete
 * 0. 阻止默认行为
 * 1. 如果光标在最后一行最后一列, 什么都不干
 * 2. 如果光标在该行最后一个位置
 *      2.1. 得到下一行的光标后的内容(left_content, 另外 也肯定是该行全部的内容)
 *      2.2. 删除下一行
 *      2.3. 修正行号 -> 暂时是这样
 *      2.4. 将 left_content 内容 追加到当前行末尾
 * 3. 其他情况下
 *      3.1. 向右删除一个字符
 */
Serval.prototype.keydownHandler.  '46': function (event) {
    event.preventDefault() /* 0 */
    var self = this
    self.allocTask(function (v_cursor) {
        var $line = v_cursor.line.$line_content
        var textContent = $line.textContent
        var logicalX = v_cursor.logicalX
        var left_content = textContent.substring(logicalX + 1, textContent.length)

        var logicalY = v_cursor.logicalY
        var max = Line.max_line_number - 1
        console.log('max', max)
        if (logicalY === max) {
            if (logicalX === textContent.length) {
                return /* 1 */
            }
        } else {
            if (logicalX === textContent.length) {

                var left_content = Line.getLineContentByLogicalY(logicalY + 1).textContent /* 2.1 */
                Line.deleteLine(logicalY + 1) /* 2.2 */
                Line.fixLineNumber(v_cursor.logicalY) /* 2.3 */

                $line.textContent = v_cursor.line.$line_content.textContent + left_content /* 2.4 */
                return
            }
        }
        $line.textContent = textContent.substring(0, logicalX) + left_content /* 3.1 */
    })
},  

看看效果的说. 很普通. 见图 5-3. 顺便又试了下 BackSpace

图5-3.gif

选区 与 selection


在进行复制等操作前. 需要让计算姬知道哪些对象需要操作. 选区就是这样一个东西. 感觉也没什么好说的.

编写选区的整体思路

鉴于常识与操作习惯. 这里规定一个光标只能有一个选区 并且 选择的内容必须是连续的.

对于一个选区... 只要

  1. 拥有起点与终点. 由于区域已经规定必须是连续的. 那么
  2. 他所包含的区域就可以计算出来.
  3. 选区中的内容才可以获得.
  4. 选区也才可以绘制出来.

但是有个问题!

可能很早之前说了.? 终点总是光标的当前位置. 所以只要记下选区的起点就行了.

然而所说的起点并不是 鼠标按下时候(onmousedown)的那个点.! 这里为了方便记录. 把鼠标按下时候的那个点叫做 基准点 ..

因为选区可能是从 基准点开始往左/左上角拉 或者 往右/右下角拉. 图5-4 这样.

图5-4.gif

需要做一个判断来确定选区真正的 起点 和 终点. 这样

  1. 绘制选区才会比较方便的找准点...
  2. 截取内容时. 才会正确?.(下面会说这个问号是为什么)

获得选区内容

观察已有的编辑器功能. 可以了解到选区的创建方式:

  1. 按下 鼠标的时候确定一个选区基准点
  2. 拖动 鼠标来选取内容. (也就是根据选区基准点 与 光标当前位置 计算选区起点与终点. 之后根据起点与终点绘制视图).
  3. 放开 鼠标后确定选取的内容. (实际上步骤同2)

这里也刚好对应三个常用的鼠标事件:

  1. 按下 - onmousedown
  2. 拖动 - onmousemove
  3. 放开 - onmouseup

具体功能逻辑的话... 其实原理很简单.. 只是由于设计上的问题. 导致代码暂时有点臃肿. 就直接通过代码来显示了.

绘制选区视图

最后是关于选区的视图...

根据选区必须是连续的特性以及盒子模型一般是矩形的特性... 可以将选区分为三种类型 以方便地适应所有情况. 见 图5-5.

图5-5.png
  1. 只有一行的: 用一个带背景颜色的<div class="selection-part" /> 并控制他的偏移量与宽度 来显示
  2. 只有二行的: 那就用二个<div class="selection-part" />
  3. 多于三行的: 无论多少行都可以用三个来表示<div class="selection-part" />

记得最初的时候创建了selected-container.

就把 <div class="selection-part" /> 放进这层中. 另外 selected-container 这名字好有问题... 不如改成 selection. 再加上这几个 container 有明确的覆盖关系... 最终会改成 selection-layer 这种形式 可能比较好一点... 这里暂时不会改

code - 获得选区内容

首先来绑定事件... 因为逻辑比较清晰啦... 这里是就直接先调用还不存在的函数... 然后再去写函数内部的具体逻辑..(从界面进入到逻辑)

@path serval/script/harusame-serval.js
// 目标代码

Serval.prototype._bindMouseEvent: function () {
    var self = this

    var isMouseDown = false

    /**
     * addEventListener 是指自己写的方法,见最下面
     * 当 mousedown 时,就对光标位置进行计算
     * 1. 取消鼠标默认的行为,否则 2 不会生效
     * 2. 让编辑器总是能够接受键盘事件
     * 3. 定位鼠标
     * 4. 设置选区基准点
     * 5. 记忆鼠标已经点击还未弹起, 用来避免鼠标没有点击就一直更新选区
     */
    addEventListener(self.$serval_container, 'mousedown', function (event) {
        event.preventDefault() /* 1 */

        self.$inputer.focus() /* 2 */

        self.allocTask(function (v_cursor) {
            v_cursor.psysicalY = event.layerY /* 3 */
            v_cursor.psysicalX = event.layerX /* 3 */

            v_cursor.setSelectionBase() /* 4 */
        })

        isMouseDown = true /* 5 */
    })

    /**
     * 这里先不管触发的频次是否频繁什么的...
     * 1. 当 mousemove 时 且 鼠标 按下 时,更新光标位置
     * 2. 更新选区起点 与 终点 与 视图
     */
    addEventListener(self.$serval_container, 'mousemove', function (event) {
        event.preventDefault()
        if (isMouseDown) { 
            self.allocTask(function (v_cursor) {
                v_cursor.psysicalY = event.layerY /* 1 */
                v_cursor.psysicalX = event.layerX /* 1 */

                v_cursor.updateSelection() /* 2 */
            })
        }
    })

    /**
     * 1. 标记鼠标已经弹起
     * 2. 更新光标位置
     * 3. 更新选区起点 与 终点 与 视图
     * 4. 当存在选区的时候
     *      4.1. 获得选区内容, ---> 这里先测试下是否能获取到选区内容 <---
     */
    addEventListener(self.$serval_container, 'mouseup', function (event) {
        event.preventDefault()

        isMouseDown = false /* 1 */

        self.allocTask(function (v_cursor) {
            v_cursor.psysicalY = event.layerY /* 2 */
            v_cursor.psysicalX = event.layerX /* 2 */

            v_cursor.updateSelection() /* 3 */

            if (v_cursor.isSelectionExist()) { /* 4 */
                console.log(v_cursor.getSelectionContent()) /* 4.1 */
            }
        })
    })
},
@path serval/script/harusame-cursor.js

/**
 * 设定选区基准点
 */
Cursor.prototype.setSelectionBase: function () {
    this.mousedown_point = {
        logicalY: this.logicalY,
        logicalX: this.logicalX,
        psysicalY: this.psysicalY,
        psysicalX: this.psysicalX
    }
},

/**
 * 更新选区
 */
Cursor.prototype.updateSelection: function () {
    this.findSelection()
    // this.updateSelectionView() 用来更新选区视图
},

/**
 * 找到选区的 起点 与 终点
 * @ 这个函数名字不怎么合适.. 而且代码很丑..
 * @ 如果把坐标单独做成一个类 就会好看很多(大概
 * @ 甚至可以比如 point_end = this.mousedown_point
 * @ 但是就现在来说的话干脆就让他更臃肿 反而好看点.(大概 
 * @ _(:3」∠)_
 */
Cursor.prototype.findSelection: function () {
    var point_start = {}
    var point_end = {}

    if (this.logicalY < this.mousedown_point.logicalY) {
        point_start.logicalY = this.logicalY
        point_start.logicalX = this.logicalX
        point_start.psysicalY = this.psysicalY
        point_start.psysicalX = this.psysicalX

        point_end.logicalY = this.mousedown_point.logicalY
        point_end.logicalX = this.mousedown_point.logicalX
        point_end.psysicalY = this.mousedown_point.psysicalY
        point_end.psysicalX = this.mousedown_point.psysicalX
    } else if (this.logicalY === this.mousedown_point.logicalY) {
        if (this.logicalX < this.mousedown_point.logicalX) {
            point_start.logicalY = this.logicalY
            point_start.logicalX = this.logicalX
            point_start.psysicalY = this.psysicalY
            point_start.psysicalX = this.psysicalX

            point_end.logicalY = this.mousedown_point.logicalY
            point_end.logicalX = this.mousedown_point.logicalX
            point_end.psysicalY = this.mousedown_point.psysicalY
            point_end.psysicalX = this.mousedown_point.psysicalX
        } else {
            point_start.logicalY = this.mousedown_point.logicalY
            point_start.logicalX = this.mousedown_point.logicalX
            point_start.psysicalY = this.mousedown_point.psysicalY
            point_start.psysicalX = this.mousedown_point.psysicalX

            point_end.logicalY = this.logicalY
            point_end.logicalX = this.logicalX
            point_end.psysicalY = this.psysicalY
            point_end.psysicalX = this.psysicalX
        }
    } else {
        point_start.logicalY = this.mousedown_point.logicalY
        point_start.logicalX = this.mousedown_point.logicalX
        point_start.psysicalY = this.mousedown_point.psysicalY
        point_start.psysicalX = this.mousedown_point.psysicalX

        point_end.logicalY = this.logicalY
        point_end.logicalX = this.logicalX
        point_end.psysicalY = this.psysicalY
        point_end.psysicalX = this.psysicalX
    }

    this.selection_start = point_start
    this.selection_end = point_end
},

/**
 * 判断是否有选区
 */
Cursor.prototype.isSelectionExist: function () {
    if (this.logicalY === this.mousedown_point.logicalY && this.logicalX === this.mousedown_point  .logicalX) {
        return false
    }
    return true
},

/**
 * 获得选区内容
 * 1. 如果选区只有一行
 *      1.1. 截取 起点 与 终点 的内容,且不需要换行
 * 2. 如果选区只有二行
 *      2.1. 截取 起点 到 起点行末尾 的内容
 *      2.2. 截取 终点行开始 到 终点 的内容,且不需要换行
 * 3. 如果选区大于二行
 *      3.1. 截取 起点 到 起点行末尾 的内容
 *      3.2. 遍历除了 起点行 与 终点行 的其他行
 *          3.2.1. 截取该行的整段内容
 *      3.3. 截取 终点行开始 到 终点 的内容,且不需要换行
 */
getSelectionContent: function () {
    var point_start = this.selection_start
    var point_end = this.selection_end

    var result = ''
    var count = point_end.logicalY - point_start.logicalY
    var start_line_text = Line.getLineContentByLogicalY(point_start.logicalY).textContent

    /* 1 */
    if (count === 0) {
        console.log('--> 选区类型 : 一行 <--')
        result += start_line_text.substring(point_start.logicalX, point_end.logicalX) /* 1.1 */

    /* 2 */
    } else if (count === 1) {
        console.log('--> 选区类型 : 二行 <--')
        result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 2.1 */

        var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
        result += end_line_text.substring(0, point_end.logicalX) /* 2.2 */

    /* 3 */
    } else {
        console.log('--> 选区类型 : 多行 <--')
        result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 3.1 */

        /* 3.2 */
        for (var i = point_start.logicalY + 1; i < point_end.logicalY; i++) {
            result += Line.getLineContentByLogicalY(i).textContent + '\n' /* 3.2.1 */
        }

        var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
        result += end_line_text.substring(0, point_end.logicalX) /* 3.3 */
    }

    return result
},

就是这样.先来调试一下.保证选区数据是返回正确的再来做视图哦... 来看看效果... 见图5-6.

图5-6.gif

... 嗯嗯.. 内容能用各种姿势获取到.~

这里额外说一下... 好早之前的版本中忘记做了单行选区的 选区起点 终点的判断
会产生比如这样的事情 textContent.substring(6, 0). 但这并没有报错...

见挺靠谱的文档 MDN 其中说到了

If indexStart is greater than indexEnd, then the effect of substring() is as if the two arguments were swapped; for example, str.substring(1, 0) == str.substring(0, 1).

感觉有点神奇... 会做这样的处理...

code - 获得选区内容

确保选区的数据获得是正确的之后. 来尝试做选区的视图部分...

简化的选区视图
编辑器中的选区视图

之前也说过.. 这里的选区最多只会划分为三段... 这是为了防止操作太多的DOM起见...(偷懒.
当然像一般的编辑器那样. 每一行单独一段高亮的选区也不是不行啦... 只是还没做就感觉会卡(hen)卡(ma)的(fan).

CSS

为了能快地看到成型后的效果. 先以最快速度把<div class="selection-part" /> 塞进 <div class="selected-container /> 中. 再来做 js 的部分.

  1. 因为选区的样式已经很直观了... 这里就先写的 css.
.selection-content {
    position: absolute;
    top: 0;
    left: 0; 

    right: 0; 
    /* 
     * 这里用 right: 0 让宽度铺满一行
     * 不用 width: 100% 是因为在个人习惯调试的时候尽量不麻烦其他元素节点的样式
     * 并且此时任意一个父类也还没有设置 overflow: hidden; 就换了个方法_(:3」∠)...
     */
    
    height: 20px;
    
    background-color: rgba(120, 120, 120, .5); // 随便挑一个常用的灰色做测试
}
  1. 再放进 <div class="selected-container /> 中. 嗯嗯..放这里
@path serval/script/harusame-template.js

Template.editor = function () {
    // ...

    /*
     * before:
     * var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'})
     */
    var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}, [
        e('div', {'class': 'selection-part'}),
        e('div', {'class': 'selection-part'}),
        e('div', {'class': 'selection-part'})
    ]))

    // ...
}

  1. 嗯嗯... 这就是效果. 见图5-7
图5-7.png
  1. 这里先模拟实际效果. 再把模拟的过程转化为用 js 来控制:
    选区的控制是通过改变 top left rightheight 来实现的.
    见图5-5 中 (这里复制过来了..
图5-5.png

先规定一下:

  • 选区中的最上面这行. 比如 1 3 7 行. 之后记为 $selection_top

  • 选区中的中间的部分. 比如 8-9 行. 之后记为 $selection_middle

  • 选区中的最下面这行. 比如 4 10 行. 之后记为 $selection_bottom

模拟过程比如像这样. 见图5-8:

图5-8.gif

可以看到由于样式方面的原因.(可能算是问题).. 计算选区视图大小的时候要额外算上行号所在的空间的宽度. 是 50px.

绘制视图的整个流程就是:

mousemove 或者 mouseup 的时候. 比对当前光标的位置 与 选区起点 是否出了偏差 (计算 logicalY)... 如果有就更新选区视图

  1. 计算并存储两点间的 Y 上的差
    var diffY = point_end.logicalY - point_start.logicalY

2.1. 如果 diffY === 0. 更新
$selection_top 的 DOM 的 top left

2.2. 如果 diffY === 1. 更新
$selection_toptop left &&
$selection_bottomtop right

2.3. 如果 diffY > 1. 更新
$selection_toptop left &&
$selection_middletop height &&
$selection_bottomtop right

JS

现在测试都基本没问题了. 如果测试内容对实际要做的东西会有干扰. 就考虑删掉哦.. 有以下这个:

还原为

然后在 template 中加入这个

@path serval/script/harusame-template.js

/**
 * 选区片段
 */
selectionPart: function () {
    return SatoriDom.compile(
        e('div', {'class': 'selection-part'})
    )
}

接下来把之前想要做的流程转换为代码..

@path serval/script/harusame-cursor.js

/**
 * 1. 光标本身的元素节点
 * 2. 之前所说的基准点可以做一个初始化.
 */
var Cursor = function (config) {
    // ...

    this.mousedown_point = {}   /* 2 */

    this.$selection_top = Template.selectionPart()
    this.$selection_middle = Template.selectionPart()
    this.$selection_bottom = Template.selectionPart()

    // ...
}

/**
 * 更新选区的 值 与 视图
 */
Cursor.prototype.updateSelection: function () {
    this.findSelection()
    this.updateSelectionView()
},

/**
 * 更新选区视图
 */
Cursor.prototype.updateSelectionView: function () {
    var point_start = this.selection_start
    var point_end = this.selection_end

    var diffY = point_end.logicalY - point_start.logicalY

    switch (diffY) {

        case 0:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display:' + 'block;'

            this.$selection_middle.style.display = 'none'

            this.$selection_bottom.style.display = 'none'

            break

        case 1:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + 0 + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display: block;'

            this.$selection_middle.style.display = 'none'

            this.$selection_bottom.style.cssText =
                'top:' + point_end.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + 50 + 'px;' +
                'display: block;'

            break

        default:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + 0 + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display: block;'

            this.$selection_middle.style.cssText =
                'top:' + (point_start.psysicalY + Line.line_height) + 'px;' +
                'left:' + 50 + 'px;' +
                'height:' + (point_end.psysicalY - point_start.psysicalY - Line.line_height) + 'px;' +
                'display: block;'

            this.$selection_bottom.style.cssText =
                'top:' + point_end.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + 50 + 'px;' +
                'display: block;'

            break
    }
},

来看看有没有问题... 见 图5-9.

图5-9.gif

嗯... 选区应该没有什么问题.

说起来示例gif 里的内容都是无意义的数字之类的...因为复制粘贴什么的还没有做..就暂时用这些代替了..

接下来可能是 复制 剪切 粘贴 Home End ↑ ↓ ← → ...
感觉内容好多... 其实感觉依旧好水_(:3」∠)...

在做完这些最基础的功能之后... 重新调整与优化代码.. 之后再做多个光标.. 代码高亮&&智能提示 之类的东西


CHANGELOG

2017年8月10日14:14:00
F 在 getSelectionContent 中 修复了多余的 \n


上一篇
#4 从零开始制作在线 代码编辑器

下一篇
#6 从零开始制作在线 代码编辑器

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

推荐阅读更多精彩内容