富文本编辑器TinyMCE的使用(React Vue)

一,需求与介绍

1.1,需求

    编辑新闻等富有个性化的文本

1.2,介绍

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。

TinyMCE的优势:

开源可商用,基于LGPL2.1

插件丰富,自带插件基本涵盖日常所需功能

接口丰富,可扩展性强,有能力可以无限拓展功能

界面好看,符合现代审美

提供经典、内联、沉浸无干扰三种模式

对标准支持优秀(自v5开始)

多语言支持,官网可下载几十种语言。

二,配置集成并组件化

2.1,通用配置

1,工具栏toolbar

复制代码

1 // Here is a list of the toolbar

2 // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols

3

4 // const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent  blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor']// fullscreen

5 const toolbar = ['code codesample undo redo restoredraft | cut copy paste pastetext | forecolor backcolor searchreplace bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | formatselect fontselect fontsizeselect | blockquote subscript superscript removeformat | table image media charmap emoticons hr pagebreak insertdatetime print preview']// | fullscreen

6 export default toolbar

复制代码

2,插件plugins

复制代码

1 // Any plugins you want to use has to be imported

2 // Detail plugins list see https://www.tinymce.com/docs/plugins/

3 // Custom builds see https://www.tinymce.com/download/custom-builds/

4

5 // const plugins = ['advlist anchor autolink autosave code codesample directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textpattern visualblocks visualchars wordcount']

6 const plugins = ['print preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount imagetools textpattern help paste emoticons autosave']

7

8 export default plugins

复制代码

3,常用字体配置fonts

复制代码

1 // Any font you want to use has to be imported

2 const fontsizeFormats='12px 14px 16px 18px 24px 36px 48px 56px 72px';

3 const fontFormats= '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats;知乎配置=BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif;小米配置=Helvetica Neue,Helvetica,Arial,Microsoft Yahei,Hiragino Sans GB,Heiti SC,WenQuanYi Micro Hei,sans-serif';

4

5 export default {

6    fontsizeFormats,

7    fontFormats

8 }

复制代码

4,准备标签

1  <div>

2              <textarea id="tinymceId"/>

3            </div>

在textarea外面需要套一层div,否则会产生一些意想不到的问题

5,初始化标签,生成编辑框

复制代码

1  window.tinymce.init({

2      language: 'zh_CN',

3      selector: `#${tinymceId}`,

4      height: height,

5      body_class: 'panel-body ',

6      object_resizing: false,

7      toolbar: toolbar.length > 0 ? toolbar : defaultToolbar,

8      menubar: menubar,

9      plugins: defaultplugins,

10      end_container_on_empty_block: true,

11      fontsize_formats: fontsizeFormats,

12      font_formats: fontFormats,

13      powerpaste_word_import: 'clean',

14      code_dialog_height: 450,

15      code_dialog_width: 1000,

16      advlist_bullet_styles: 'square',

17      advlist_number_styles: 'default',

18      imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

19      default_link_target: '_blank',

20      link_title: false,

21      nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

22      init_instance_callback: editor => {

23        if (content) {

24          editor.setContent(content)

25        }

26        editor.on('NodeChange Change KeyUp SetContent', () => {

27        })

28      },

29      setup(editor) {

30        editor.on('FullscreenStateChanged', (e) => {

31        })

32      }

33    })

复制代码

2.2,React组件化

直接放代码

复制代码

  1 import React from 'react';

  2 import { Button } from 'antd';

  3 import PropTypes from 'prop-types';

  4 import styles from './index.less';

  6 import defaultplugins from './plugins';

  7 import defaultToolbar from './toolbar';

  8 import {

  9  fontsizeFormats,

10  fontFormats

11 } from './font';

12 import UploadImage from './UploadImage';

13

14

15 class Tinymce extends React.Component {

16

17  static propTypes = {

18    tinymceId: PropTypes.string,

19    content: PropTypes.string,

20    toolbar: PropTypes.array,

21    menubar: PropTypes.string,

22    height: PropTypes.number,

23    getContent: PropTypes.func,

24  };

25  static defaultProps = {

26    tinymceId: 'react-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + ''),

27    menubar: 'file edit insert view format table',

28    height: 520,

29    toolbar: []

30  };

31  constructor(props) {

32    super(props);

33    this.state = {

34      hasChange: false,

35      hasInit: false,

36      fullscreen: false,

37    };

38  };

39

40  componentDidMount() {

41    this.initTinymce()

42

43  }

44  componentWillUnmount() {

45    this.destroyTinymce()

46  }

47  initTinymce() {

48    const { tinymceId, menubar, height, toolbar, content, getContent } = this.props

49    const _this = this

50    window.tinymce.init({

51      language: 'zh_CN',

52      selector: `#${tinymceId}`,

53      height: height,

54      body_class: 'panel-body ',

55      object_resizing: false,

56      toolbar: toolbar.length > 0 ? toolbar : defaultToolbar,

57      menubar: menubar,

58      plugins: defaultplugins,

59      end_container_on_empty_block: true,

60      fontsize_formats: fontsizeFormats,

61      font_formats: fontFormats,

62      powerpaste_word_import: 'clean',

63      code_dialog_height: 450,

64      code_dialog_width: 1000,

65      advlist_bullet_styles: 'square',

66      advlist_number_styles: 'default',

67      imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

68      default_link_target: '_blank',

69      link_title: false,

70      nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

71      init_instance_callback: editor => {

72        if (content) {

73          editor.setContent(content)

74        }

75        _this.setState({

76          hasInit: true

77        })

78        editor.on('NodeChange Change KeyUp SetContent', () => {

79          _this.setState({

80            hasChange: true

81          })

82        })

83      },

84      setup(editor) {

85        editor.on('FullscreenStateChanged', (e) => {

86          _this.setState({

87            fullscreen: e.state

88          })

89        })

90      }

91    })

92  }

93  destroyTinymce() {

94    const { tinymceId } = this.props

95    const { fullscreen } = this.state

96    const tinymce = window.tinymce.get(tinymceId)

97    if (fullscreen) {

98      tinymce.execCommand('mceFullScreen')

99    }

100

101    if (tinymce) {

102      tinymce.destroy()

103    }

104  }

105  // setContent(value) {

106  //  const { tinymceId } = this.props

107  //  window.tinymce.get(tinymceId).setContent(value)

108  // }

109  saveToGetContent() {

110    const { tinymceId, getContent } = this.props

111    if (getContent && typeof getContent === 'function') {

112      getContent(window.tinymce.get(tinymceId).getContent())

113    }

114  }

115

116  /**

117    * 上传图片成功回调

118    *  */

119  imageSuccessCBK(arr) {

120    const { tinymceId } = this.props

121    arr.forEach(v => {

122      window.tinymce.get(tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)

123    })

124  }

125  render() {

126    const { loading, tinymceId } = this.props

127    const { fullscreen } = this.state

128    const header = (

129      <Page.Header breadcrumb={['富文本实例']} title={'富文本实例'} />

130    );

131    return (

132      <div className={styles['tinymce-components']}>

133        <Page header={header} loading={!!loading}>

134

135          <div className={fullscreen ? "tinymce-container mce-fullscreen" : "tinymce-container"}>

136

137            <div>

138              <textarea id={tinymceId} className={styles['tinymce-textarea']} />

139            </div>

140            <div className="editor-custom-btn-container">

141              <UploadImage className="editor-upload-btn" imageSuccessCBK={(arr)=>{this.imageSuccessCBK(arr)}}/>

142            </div>

143            <Button type="primary" onClick={() => { this.saveToGetContent() }}>保存</Button>

144          </div>

145        </Page>

146      </div>

147    );

148  }

149 }

150

151 export default Tinymce;

复制代码

上传图片组件,使用antd的部分组件:

复制代码

  1 import React from 'react';

  2 import { Button, Modal, Icon, Upload, message } from 'antd';

  3 import PropTypes from 'prop-types';

  4 import styles from './index.less';

  5

  6 const Dragger = Upload.Dragger;

  7

  8 class UploadImage extends React.Component {

  9

10  static propTypes = {

11    imageSuccessCBK: PropTypes.func,

12

13  };

14  static defaultProps = {

15

16  };

17  constructor(props) {

18    super(props);

19    this.state = {

20      visible: false,

21      listObj: {}

22    };

23  };

24

25  /**

26    * 显示弹框

27    *

28    *  */

29  showModal = () => {

30    this.setState({

31      visible: true,

32    });

33  }

34

35  /**

36  * 确认

37  *

38  *  */

39  handleOk = (e) => {

40    const { imageSuccessCBK } = this.props

41    const { listObj } = this.state

42    const imagesFileArr = Object.keys(listObj).map(v => listObj[v])

43    imageSuccessCBK(imagesFileArr)

44    this.setState({

45      visible: false,

46      listObj: {},

47      Uploading: false

48    });

49  }

50

51  handleCancel = (e) => {

52    this.setState({

53      visible: false,

54      listObj: {}

55    });

56  }

57  render() {

58    const { loading } = this.props

59    const { visible, listObj, Uploading } = this.state

60    const props = {

61      name: 'file',

62      multiple: true,

63      action: '//jsonplaceholder.typicode.com/posts/',

64      listType: 'picture',

65      onChange: (info) => {

66        const uid = info.file.uid

67        const objKeyArr = Object.keys(listObj)

68        const status = info.file.status;

69        if (status !== 'uploading') {

70          console.log(info.file, info.fileList);

71        }

72        if (status === 'done') {//已成功上传

73          this.setState({

74            Uploading: false,

75          })

76          for (let i = 0, len = objKeyArr.length; i < len; i++) {

77            if (listObj[objKeyArr[i]].uid === uid) {

78              listObj[objKeyArr[i]].url = info.file.thumbUrl

79              listObj[objKeyArr[i]].hasSuccess = true

80              message.success(`${info.file.name} file uploaded successfully.`);

81              return

82            }

83          }

84

85        } else if (status === 'error') {

86          this.setState({

87            Uploading: false,

88          })

89          message.error(`${info.file.name} file upload failed.`);

90        }

91        if (status === 'removed') {//移除上传的

92          for (let i = 0, len = objKeyArr.length; i < len; i++) {

93            if (listObj[objKeyArr[i]].uid === uid) {

94              delete listObj[objKeyArr[i]]

95              message.success(`${info.file.name} file removed successfully.`);

96              return

97            }

98          }

99        }

100      },

101      beforeUpload: (file) => {

102        this.setState({

103          Uploading: true,

104        })

105        const _self = this

106        const _URL = window.URL || window.webkitURL

107        const fileName = file.uid

108        listObj[fileName] = {}

109        return new Promise((resolve, reject) => {

110          const img = new Image()

111          img.src = _URL.createObjectURL(file)

112          img.onload = function () {

113            listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }

114            _self.setState({

115              listObj,

116            })

117          }

118          resolve(true)

119        })

120      },

121    };

122

123    return (

124      <div>

125        <Button

126          style={{ marginTop: 0 }}

127          type="primary"

128          shape="round"

129          icon="upload"

130          onClick={() => { this.showModal() }}>

131          上传

132            </Button>

133        {

134          visible ? <Modal

135            title="上传图片"

136            visible={visible}

137            onCancel={this.handleCancel}

138            footer={[

139              <div key="1">

140                <Button onClick={() => this.handleCancel()} loading={!!Uploading}>取消</Button>

141                <Button type="primary" style={{ marginLeft: 8 }} onClick={() => this.handleOk()} loading={!!Uploading}>

142                  确定

143                </Button>

144              </div>]}

145          >

146            <Dragger {...props}>

147              <p className="ant-upload-drag-icon">

148                <Icon type="inbox" />

149              </p>

150              <p className="ant-upload-text">Click or drag file to this area to upload</p>

151              <p className="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>

152            </Dragger>

153          </Modal> : null

154        }

155      </div>

156    );

157  }

158 }

159

160 export default UploadImage;

复制代码

2.3,Vue组件化

  直接放代码

复制代码

  1 <template>

  2  <div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">

  3    <div>

  4    <textarea :id="tinymceId" class="tinymce-textarea" />

  5    </div>

  6    <div class="editor-custom-btn-container">

  7      <editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />

  8    </div>

  9  </div>

10 </template>

11

12 <script>

13 import editorImage from './components/EditorImage'

14 import plugins from './plugins'

15 import toolbar from './toolbar'

16 import font from './font';

17

18 export default {

19  name: 'Tinymce',

20  components: { editorImage },

21  props: {

22    id: {

23      type: String,

24      default: function() {

25        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')

26      }

27    },

28    value: {

29      type: String,

30      default: ''

31    },

32    toolbar: {

33      type: Array,

34      required: false,

35      default() {

36        return []

37      }

38    },

39    menubar: {

40      type: String,

41      default: 'file edit insert view format table'

42    },

43    height: {

44      type: Number,

45      required: false,

46      default: 520

47    }

48  },

49  data() {

50    return {

51      hasChange: false,

52      hasInit: false,

53      tinymceId: this.id,

54      fullscreen: false,

55      languageTypeList: {

56        'en': 'en',

57        'zh': 'zh_CN'

58      }

59    }

60  },

61  computed: {

62    language() {

63      return this.languageTypeList[this.$store.getters.language]

64    }

65  },

66  watch: {

67    value(val) {

68      if (!this.hasChange && this.hasInit) {

69        this.$nextTick(() =>

70          window.tinymce.get(this.tinymceId).setContent(val || ''))

71      }

72    },

73    language() {

74      this.destroyTinymce()

75      this.$nextTick(() => this.initTinymce())

76    }

77  },

78  mounted() {

79    this.initTinymce()

80  },

81  activated() {

82    this.initTinymce()

83  },

84  deactivated() {

85    this.destroyTinymce()

86  },

87  destroyed() {

88    this.destroyTinymce()

89  },

90  methods: {

91    initTinymce() {

92      const _this = this

93      window.tinymce.init({

94        language: 'zh_CN',

95        selector: `#${this.tinymceId}`,

96        height: this.height,

97        body_class: 'panel-body ',

98        object_resizing: false,

99        toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,

100        menubar: this.menubar,

101        plugins: plugins,

102        end_container_on_empty_block: true,

103        fontsize_formats: font.fontsizeFormats,

104        font_formats: font.fontFormats,

105        powerpaste_word_import: 'clean',

106        code_dialog_height: 450,

107        code_dialog_width: 1000,

108        advlist_bullet_styles: 'square',

109        advlist_number_styles: 'default',

110        imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],

111        default_link_target: '_blank',

112        link_title: false,

113        nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin

114        init_instance_callback: editor => {

115          if (_this.value) {

116            editor.setContent(_this.value)

117          }

118          _this.hasInit = true

119          editor.on('NodeChange Change KeyUp SetContent', () => {

120            this.hasChange = true

121            this.$emit('input', editor.getContent())

122          })

123        },

124        setup(editor) {

125          editor.on('FullscreenStateChanged', (e) => {

126            _this.fullscreen = e.state

127          })

128        }

129      })

130    },

131    destroyTinymce() {

132      const tinymce = window.tinymce.get(this.tinymceId)

133      if (this.fullscreen) {

134        tinymce.execCommand('mceFullScreen')

135      }

136

137      if (tinymce) {

138        tinymce.destroy()

139      }

140    },

141    setContent(value) {

142      window.tinymce.get(this.tinymceId).setContent(value)

143    },

144    getContent() {

145      window.tinymce.get(this.tinymceId).getContent()

146    },

147    imageSuccessCBK(arr) {

148      const _this = this

149      arr.forEach(v => {

150        window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)

151      })

152    }

153  }

154 }

155 </script>

156

157 <style scoped>

158 .tinymce-container {

159  position: relative;

160  line-height: normal;

161 }

162 .tinymce-container>>>.mce-fullscreen {

163  z-index: 10000;

164 }

165 .tinymce-textarea {

166  visibility: hidden;

167  z-index: -1;

168 }

169 .editor-custom-btn-container {

170  position: absolute;

171  right: 4px;

172  top: 4px;

173  /*z-index: 2005;*/

174 }

175 .fullscreen .editor-custom-btn-container {

176  z-index: 10000;

177  position: fixed;

178 }

179 .editor-upload-btn {

180  display: inline-block;

181 }

182 </style>

复制代码

上传图片组件,使用elementUI的部分组件:

复制代码

  1 <template>

  2  <div class="upload-container">

  3    <el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">

  4      upload

  5    </el-button>

  6    <el-dialog :visible.sync="dialogVisible">

  7      <el-upload

  8        :multiple="true"

  9        :file-list="fileList"

10        :show-file-list="true"

11        :on-remove="handleRemove"

12        :on-success="handleSuccess"

13        :before-upload="beforeUpload"

14        class="editor-slide-upload"

15        action="https://httpbin.org/post"

16        list-type="picture-card"

17      >

18        <el-button size="small" type="primary">

19          Click upload

20        </el-button>

21      </el-upload>

22      <el-button @click="dialogVisible = false">

23        Cancel

24      </el-button>

25      <el-button type="primary" @click="handleSubmit">

26        Confirm

27      </el-button>

28    </el-dialog>

29  </div>

30 </template>

31

32 <script>

33 // import { getToken } from 'api/qiniu'

34

35 export default {

36  name: 'EditorSlideUpload',

37  props: {

38    color: {

39      type: String,

40      default: '#1890ff'

41    }

42  },

43  data() {

44    return {

45      dialogVisible: false,

46      listObj: {},

47      fileList: []

48    }

49  },

50  methods: {

51    checkAllSuccess() {

52      return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)

53    },

54    handleSubmit() {

55      const arr = Object.keys(this.listObj).map(v => this.listObj[v])

56      if (!this.checkAllSuccess()) {

57        this.$message('Please wait for all images to be uploaded successfully. If there is a network problem, please refresh the page and upload again!')

58        return

59      }

60      this.$emit('successCBK', arr)

61      this.listObj = {}

62      this.fileList = []

63      this.dialogVisible = false

64    },

65    handleSuccess(response, file) {

66      const uid = file.uid

67      const objKeyArr = Object.keys(this.listObj)

68      for (let i = 0, len = objKeyArr.length; i < len; i++) {

69        if (this.listObj[objKeyArr[i]].uid === uid) {

70          this.listObj[objKeyArr[i]].url = response.files.file

71          this.listObj[objKeyArr[i]].hasSuccess = true

72          return

73        }

74      }

75    },

76    handleRemove(file) {

77      const uid = file.uid

78      const objKeyArr = Object.keys(this.listObj)

79      for (let i = 0, len = objKeyArr.length; i < len; i++) {

80        if (this.listObj[objKeyArr[i]].uid === uid) {

81          delete this.listObj[objKeyArr[i]]

82          return

83        }

84      }

85    },

86    beforeUpload(file) {

87      const _self = this

88      const _URL = window.URL || window.webkitURL

89      const fileName = file.uid

90      this.listObj[fileName] = {}

91      return new Promise((resolve, reject) => {

92        const img = new Image()

93        img.src = _URL.createObjectURL(file)

94        img.onload = function() {

95          _self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }

96        }

97        resolve(true)

98      })

99    }

100  }

101 }

102 </script>

103

104 <style lang="scss" scoped>

105 .editor-slide-upload {

106  margin-bottom: 20px;

107  /deep/ .el-upload--picture-card {

108    width: 100%;

109  }

110 }

111 </style>

复制代码

三,使用

3.1,React

第一步:导入组件

1 import Tinymce from '../../components/Tinymce';

第二步:使用组件

1  <Tinymce

2            content={''}

3              tinymceId='tinymceIdDemo'

4              getContent={(content) => { this.getContent(content) }}

5            />

第三步:获取输入的富文本

复制代码

1  getContent(content) {

2    console.log('content===',content)

3    this.setState({

4      content

5    })

6  }

复制代码

第四步:文本渲染

1  {/* 渲染标签字符串 */}

2            <div dangerouslySetInnerHTML={{ __html: content }}></div>

东莞网站建设www.zg886.cn

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

推荐阅读更多精彩内容