Activiti 7 学习整合 BPMN.js(二)

动态表当读取历史数据接口

  • 我们在上面的渲染动态表单接口 (formDataShow)方法中进行扩充,大致思路是在渲染表单之前会根据 taskId 查出流程 id,并且根据流程实例 id 查出所有表单填写的值,将这些值构建成一个 hashMap 字典,这样就是一个 key-value 的形式,那么接下来如果我们的表单默认值如果不是一个字符串而是一个以 FormProperty 开头的其他表单的名称,我们就从这个字典里面通过 key 将之前存的值读取出来并且渲染出来,这就是我们的大致思路
ActivitiMapper 中编写查询表单历史数据的方法
    /**
     * 读取表单数据
     * @param PROC_INST_ID   流程实例 id
     * @return
     */
    @Select("select Control_ID_,Control_VALUE_ from formdata where PROC_INST_ID_ = #{PROC_INST_ID}")
    public List<Map<String,String>> selectFormData(@Param("PROC_INST_ID")String PROC_INST_ID);
formDataShow 方法中读取表单历史数据,并保存在数据字典中
/*
                构建表单控件历史数据字典
                key 是控件的 id
                value 是控件的值
                这里是我们根据 taskId 查询出来的所有流程的表单数据
            */
            Map<String,String> controlListMap = new HashMap<String, String>();
            // 读取数据库本流程实例的所有表单数据
            List<Map<String, String>> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
            // 将查询出来的控件 id 和值保存在数据字典中
            for(Map<String,String> map : tempControlList){
                controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
            }
之前返回给前端的默认值是写死的现在需要,如果默认值保存的是之前任意环节的控件 id 时,需要读取出之前控件的值
// 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
                if(split[3].startsWith("FormProperty_")){
                    // 是表单数据  从之前的保存的所有控件历史数据字典中读取
                    // 在这里  split[3] 拿到的就是历史表单的 key
                    if(controlListMap.containsKey(split[3])){
                        form.put("controlDefValue", controlListMap.get(split[3]));
                    }else{
                        // 如果字典中不存在给出错误提示
                        form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
                    }
                }else{
                    // 不是之前表单数据
                    form.put("controlDefValue", split[3]);
                }
画 bpmn js 测试,我们需要在 A 环节填写的数据在 B 环节读取出来
image.png

image.png
  • 张三表单的数据
    FormProperty_26555eg-_-string-_-姓名-_-请输入姓名-_-f
    FormProperty_0c4etf8-_-string-_-姓别-_-男或女-_-f


    image.png

    image.png
  • 李四的表单数据
    李四自己的表单
    FormProperty_0gvpb9n-_-string-_-姓名-_-我是张三-_-f
    读取张三表单填写的内容,FormProperty_0c4etf8 这个保存的就是张三表单的key 通过它就可以读取到张三填写的值
    FormProperty_11muvfh-_-string-_-姓别-_-FormProperty_0c4etf8-_-f
bpmn js 创建好之后,上传,部署,略过......
查询流程
  • 张三填写数据的任务


    image.png
  • 模拟张三提交表单
  • 张三表单数据 这个 “女” 就是李四需要读取出来的
    FormProperty_26555eg-_-我是张三-_-f!!FormProperty_0c4etf8-_-女--s
    image.png
  • 切换用户查看渲染李四的表单数据,可以看到张三表单中填写的数据被读取出来了


    image.png
渲染表单 formDataShow 完整代码
    /**
     * 渲染动态表单
     * @return
     */
    @GetMapping("/formDataShow")
    public AjaxResponse formDataShow(@RequestParam("taskId")String taskId){

        try{
            // 这是 GlobalConfig 类定义的是否是测试标记,标记是测试环境使用 内存用户登录,方便测试使用
            if(GlobalConfig.Test){
                // 测试环境使用内存用户登录
                securityUtil.logInAs("zhangsan");
            }
            // 查询任务
            Task task = taskRuntime.task(taskId);

            /*
                构建表单控件历史数据字典
                key 是控件的 id
                value 是控件的值
                这里是我们根据 taskId 查询出来的所有流程的表单数据
            */
            Map<String,String> controlListMap = new HashMap<String, String>();
            // 读取数据库本流程实例的所有表单数据
            List<Map<String, String>> tempControlList = activitiMapper.selectFormData(task.getProcessInstanceId());
            // 将查询出来的控件 id 和值保存在数据字典中
            for(Map<String,String> map : tempControlList){
                controlListMap.put(map.get("Control_ID_").toString(), map.get("Control_VALUE_").toUpperCase());
            }

            /**
             * 关键代码,这里在强调一下,在 activiti6 和 5 的时候实际上是有 form 这个类的
             * 但是在 activiti7 中去掉的为了轻量化,但是我们在 7 中还是有方法可以通过流程
             * 定义的 id 和任务的 id 拿到一个叫 userTask 的类,在 userTask 类中可以拿到表单属性
             */
            UserTask userTask = (UserTask) repositoryService.getBpmnModel(task.getProcessDefinitionId())
                    /**
                     * 获取流程元素,这里是要传什么呢?这里实际上是要传任务的key的,在 task 里并没有任务的 key 的
                     * 在 activiti 6 中是有任务的 key 的,不过我们可以用另外一个方案,我们可以用表单的 key ,这里
                     * 我们可以表表单的 key 和任务的 key 启成一模一样的名字,这样你拿表单的 key 就相当于拿任务的 key
                     * 了
                     */
                    .getFlowElement(task.getFormKey());
            // 说明该环节是不需要表单的
            if(userTask == null){
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.SUCCESS.getCode(),
                        GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                        "无表单"
                );
            }

            List<FormProperty> formProperties = userTask.getFormProperties();
            // 保存分割后的格式返回给前端
            List<Map<String,Object>> listMap = new ArrayList<Map<String, Object>>();
            for (FormProperty formProperty : formProperties) {
                // 分割表单数据
                String[] split = formProperty.getId().split("-_-");
                Map<String,Object> form = new HashMap<String,Object>();
                form.put("id", split[0]);
                form.put("controlType", split[1]);
                form.put("controlLabel", split[2]);

                // 如果默认值是之前任意表单控件的 id ,那么我们应该读取之前表单的值
                if(split[3].startsWith("FormProperty_")){
                    // 是表单数据  从之前的保存的所有控件历史数据字典中读取
                    // 在这里  split[3] 拿到的就是历史表单的 key
                    if(controlListMap.containsKey(split[3])){
                        form.put("controlDefValue", controlListMap.get(split[3]));
                    }else{
                        // 如果字典中不存在给出错误提示
                        form.put("controlDefValue", "读取失败,检查"+split[3]+"配置");
                    }
                }else{
                    // 不是之前表单数据
                    form.put("controlDefValue", split[3]);
                }

                form.put("controlParam", split[4]);
                listMap.add(form);
            }
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    listMap
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "渲染动态表单失败",
                    e.toString()
            );
        }
    }

高亮历史流程渲染

效果

image.png

先画流程图,上传,部署,启动和之前的一样,这里略过

image.png

获取需要高亮的连线 id 编号方法

// 获取一条流程实例历史
            HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(instanceId)
                    .singleResult();
            // 根据流程定义 key 获取 BMPN
            BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
            // 获取流程
            Process process = bpmnModel.getProcesses().get(0);
            // 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
            Collection<FlowElement> flowElements = process.getFlowElements();

            /**
             * 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
             * value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
             * 就可以获取到需要高亮的连线
             */
            Map<String,String> map = new HashMap<String,String>();
            for (FlowElement flowElement : flowElements) {
                // 判断是否是线条
                if(flowElement instanceof SequenceFlow){
                    SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
                    String ref = sequenceFlow.getSourceRef();
                    String targetRef = sequenceFlow.getTargetRef();
                    /**
                     * 保存开始节点和结束节点,与它们之间连线的对应关系
                     * key: 开始节点 编号 + 结束节点 编号
                     * value: 连线编号
                     */
                    map.put(ref+targetRef,sequenceFlow.getId());
                }
            }

            /**
             * 获取已经完成的全部流程历史节点
             */
            List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .list();
            /**
             * 将各个历史节的开始节点和结束几点的编号两两对应起来,
             * 就可以从上面的 map 中获取到需要高亮的连线
             */
            Set<String> keyList = new HashSet<String>();
            for (HistoricActivityInstance i : list) {
                for (HistoricActivityInstance j : list) {
                    if(i != j){
                        keyList.add(i.getActivityId()+j.getActivityId());
                    }
                }
            }

            // 获取高亮连线 id
            Set<String> highLine = new HashSet<String>();
            // 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
            keyList.forEach(s -> highLine.add(map.get(s)));
image.png
最终获取到的连线,因为 任务1 我已经执行过了所以获取到的是两根连线编号
image.png

获取需要高亮已经完成的任务节点编号方法

            // 获取已经完成的节点
            List<HistoricActivityInstance> listFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 已经完成的节点高亮
            Set<String> highPoint = new HashSet<>();
            // 保存已经完成的流程节点编号
            listFinished.forEach(s -> highPoint.add(s.getActivityId()));
这时获取到的是 开始节点 和 任务1 节点,因为任务一我已经执行过了
image.png

获取需要高亮下一步我要代办执行的任务节点编号方法

            // 获取代办节点
            List<HistoricActivityInstance> listUnFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .unfinished()
                    .list();

            // 代办的节点高亮
            Set<String> waitingToDo = new HashSet<>();
            // 保存需要代办的节点编号
            listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));
这时获取到的就是 任务2 节点编号,因为 任务1 执行完之后就流转到 任务2 了
image.png

获取当前用户已经完成的任务节点编号

            // 获取当前用户完成的任务
            List<HistoricTaskInstance> taskInstanceList = historyService.createHistoricTaskInstanceQuery()
                    .taskAssignee(userInfoBean.getUsername())
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 当前用户完成的高亮
            Set<String> iDo = new HashSet<String>();
            // 保存用户完成的节点编号
            taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));
当前用户完成了 任务1
image.png
最终得到的数据
{
    "status": 0,
    "msg": "成功",
    "obj": {
        "waitingToDo": [
            "Activity_2"
        ],
        "highPoint": [
            "StartEvent_1",
            "Activity_1"
        ],
        "iDo": [
            "Activity_1"
        ],
        "highLine": [
            null,
            "Flow_2",
            "Flow_1"
        ]
    }
}

完整方法

    /**
     * 高亮显示路程历史
     * @param instanceId     流程实例id
     * @param userInfoBean   用户信息
     * @return
     */
    @GetMapping("/getHighlight")
    public AjaxResponse getHighlight(@RequestParam("instanceId")String instanceId,
                                     @AuthenticationPrincipal UserInfoBean userInfoBean){
        try {
            // 获取一条流程实例历史
            HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
                    .processInstanceId(instanceId)
                    .singleResult();
            // 根据流程定义 key 获取 BMPN
            BpmnModel bpmnModel = repositoryService.getBpmnModel(historicProcessInstance.getProcessDefinitionId());
            // 获取流程
            Process process = bpmnModel.getProcesses().get(0);
            // 获取所有流程 FlowElement 的信息,就是所有bpmn的节点
            Collection<FlowElement> flowElements = process.getFlowElements();

            /**
             * 这个 map 中的 key 就是 开始节点和结束节点的编号拼起来的字符串,
             * value 就是开始节点和结束节点的连线,到时候我们根据开始节点和结束节点
             * 就可以获取到需要高亮的连线
             */
            Map<String,String> map = new HashMap<String,String>();
            for (FlowElement flowElement : flowElements) {
                // 判断是否是线条
                if(flowElement instanceof SequenceFlow){
                    SequenceFlow sequenceFlow = (SequenceFlow)flowElement;
                    String ref = sequenceFlow.getSourceRef();
                    String targetRef = sequenceFlow.getTargetRef();
                    /**
                     * 保存开始节点和结束节点,与它们之间连线的对应关系
                     * key: 开始节点 编号 + 结束节点 编号
                     * value: 连线编号
                     */
                    map.put(ref+targetRef,sequenceFlow.getId());
                }
            }

            /**
             * 获取已经完成的全部流程历史节点
             */
            List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .list();
            /**
             * 将各个历史节的开始节点和结束几点的编号两两对应起来,
             * 就可以从上面的 map 中获取到需要高亮的连线
             */
            Set<String> keyList = new HashSet<String>();
            for (HistoricActivityInstance i : list) {
                for (HistoricActivityInstance j : list) {
                    if(i != j){
                        keyList.add(i.getActivityId()+j.getActivityId());
                    }
                }
            }

            // 获取高亮连线 id
            Set<String> highLine = new HashSet<String>();
            // 根据已经完成的(开始节点编号+结束节点编号)组成的 key 获取需要高亮的连线编号
            keyList.forEach(s -> highLine.add(map.get(s)));


            // 获取已经完成的节点
            List<HistoricActivityInstance> listFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 已经完成的节点高亮
            Set<String> highPoint = new HashSet<>();
            // 保存已经完成的流程节点编号
            listFinished.forEach(s -> highPoint.add(s.getActivityId()));

            // 获取代办节点
            List<HistoricActivityInstance> listUnFinished = historyService.createHistoricActivityInstanceQuery()
                    .processInstanceId(instanceId)
                    .unfinished()
                    .list();

            // 代办的节点高亮
            Set<String> waitingToDo = new HashSet<>();
            // 保存需要代办的节点编号
            listUnFinished.forEach(s -> waitingToDo.add(s.getActivityId()));


            // 获取当前用户完成的任务
            List<HistoricTaskInstance> taskInstanceList = historyService.createHistoricTaskInstanceQuery()
                    .taskAssignee(userInfoBean.getUsername())
                    .processInstanceId(instanceId)
                    .finished()
                    .list();

            // 当前用户完成的高亮
            Set<String> iDo = new HashSet<String>();
            // 保存用户完成的节点编号
            taskInstanceList.forEach(s -> iDo.add(s.getTaskDefinitionKey()));

            Map<String,Object> reMap = new HashMap<String, Object>();
            // 高亮已经完成的节点
            reMap.put("highPoint",highPoint);
            // 高亮连线节点编号
            reMap.put("highLine", highLine);
            // 高亮代办节点编号
            reMap.put("waitingToDo", waitingToDo);
            // 高亮当前用户完成的节点编号
            reMap.put("iDo",iDo);

            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    reMap
            );
        } catch (Exception e) {
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "高亮历史任务失败",
                    e.toString()
            );
        }
    }

bpmn js 扩展下载

在 bpmnjs 目录下 app 目录下的 index.html 中添加导出按钮

    <li>
      <a id="downloadBPNM" href title="download as SVG image">
        导出
      </a>
    </li>

创建 tools.js

image.png
import $ from 'jquery'
const proHost = window.location.protocol + "//" + window.location.host;
const href = window.location.href.split("bpmnjs")[0];
const key = href.split(window.location.host)[1];
const publicurl = proHost + key;
const tools = {
    /**
     * 下载方法
     * @param bpmnModeler
     */
    download(bpmnModeler){
        var downloadLink = $("#downloadBPNM");
        bpmnModeler.saveXML({format:true},function(err,xml){
            if(err){
                return console.error("could not save bpmn",err);
            }
            tools.setEncoded(downloadLink,"digaram.bpmn",xml);
        });
    },
    /**
     *
     * @param link  下载的按钮
     * @param name  下载的名字
     * @param data  下载的数据
     */
    setEncoded(link, name, data) {
     var encodedData = encodeURIComponent(data);

        if (data) {
            link.addClass('active').attr({
                'href': 'data:application/bpmn20-xml;charset=UTF-8,' + encodedData,
                'download': name
            });
        } else {
            link.removeClass('active');
        }
    }
}

export default tools

在 index.html 同级目录的 index.js 中引入创建的 tools.js

image.png

在 index.html 同级目录的 index.js 添加导出按钮的点击事件,bpmnModeler 就是 bpmn 的模型

  $("#downloadBPNM").on('click',function(){
    tools.download(bpmnModeler);
  });
image.png

测试 正常导出

image.png

在线部署 bpmn

index.html 中添加部署按钮

    <li>
      <a id="saveBPNM" href>
        部署
      </a>
    </li>

index.js 中添加部署的点击事件

// 部署 bpmn
  $("#saveBPNM").on('click',function(){
    tools.saveBPMN(bpmnModeler);
  });

tools.js 中添加在线部署的方法

/**
     * 部署方法
     * @param bpmnModeler
     */
    saveBPMN(bpmnModeler){
        var downloadLink = $("#downloadBPNM");
        bpmnModeler.saveXML({format:true},function(err,xml){
            if(err){
                return console.error("could not save bpmn",err);
            }
            console.info(xml)
            // 参数就是 bpmn xml 字符串
            var param = {
                "xmlBPMN": xml
            };
            // 调用后台接口上传bpmn
            $.ajax({
                url: publicurl + "processDefinition/addDeploymentByString",
                type: "post",
                dataType: "json",
                data: param,
                success: function(res){
                    if(res.status == 0){
                        alert("部署成功");
                    }else{
                        alert("部署失败");
                    }
                },
                error: function(err){
                    console.info(err);
                }
            });
        });
    }

上传 BPMN 并展示

index.html 添加导入按钮

    <!--  导入 bpmn  -->
    <li>
      <form id="form" name="myForm" onsubmit="return false" method="post" enctype="multipart/form-data" title="上传文件">
        <input type="file" name="uploadFile" id="uploadFile" accept=".bpmn" style="display:none"/>
        <label class="label" for="uploadFile">导入</label>
      </form>
    </li>

index.js 添加导入按钮变事件

  // 上传 bpmn
  $("#uploadFile").on("change",function(i){
    tools.uploadBPMN(bpmnModeler);
  });

tools.js 中编写 uploadBPMN 上传 bpmn 方法

    /**
     * 上传 bpmn
     */
    uploadBPMN(bpmnModeler){
        // 获取文件
        var fileUpload = document.myForm.uploadFile.files[0];
        // 创建 FormData 对象
        var fm = new FormData();
        fm.append("processFile",fileUpload)
        $.ajax({
            url: publicurl + "processDefinition/uploadBPMN",
            type: "post",
            data: fm,
            async: false,
            contentType: false,
            processData: false,
            success: function(res){
                if(res.status == 0){
                    var url = publicurl + "bpmn/" + res.obj;
                    // 打开上传的 bpmn
                    tools.openBPMN_URL(bpmnModeler,url);
                }else{
                    alert(res.msg);
                }
            }
        });
    },
    /**
     * 打开上传的 bpmn
     * @param url
     */
    openBPMN_URL(bpmnModeler,url){
        $.ajax(url,{dataType: "text"}).done(
            // 返回 xml 文件
            async function (xml) {
                try {
                    // 导入 xml
                    await bpmnModeler.importXML(xml);

                }catch (e) {
                    console.error(e);
                }
            });
    }

编写后台上传接口,uploadBpmnPath 是上传的文件目录在 yml 中配置的换成自己的就可以了

    /**
     * 添加流程定义通过在线提交 BPMN 的 xml
     * @param processFile
     * @return
     */
    @PostMapping("/uploadBPMN")
    public AjaxResponse addDeploymentByString(HttpServletRequest request,
                                              @RequestParam("processFile") MultipartFile processFile
                                              ){
        try{
            if(processFile.isEmpty()){
                return AjaxResponse.AjaxData(
                        GlobalConfig.ResponseCode.ERROR.getCode(),
                        GlobalConfig.ResponseCode.ERROR.getDesc(),
                        "BPMN 不能为空"
                );
            }
            // 获取原始文件名
            String originFileName = processFile.getOriginalFilename();
            // 获取文件后缀
            String suffixName = originFileName.substring(originFileName.lastIndexOf("."));
            // 新的文件名
            String fileName = UUID.randomUUID() + suffixName;
            // 上传文件的路径
            File filePath = new File(uploadBpmnPath+fileName);
            if(!filePath.getParentFile().exists()){
                filePath.getParentFile().mkdirs();
            }
            // 上传
            processFile.transferTo(filePath);
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.SUCCESS.getCode(),
                    GlobalConfig.ResponseCode.SUCCESS.getDesc(),
                    fileName
            );
        }catch (Exception e){
            return AjaxResponse.AjaxData(
                    GlobalConfig.ResponseCode.ERROR.getCode(),
                    "上传 BPMN 失败",
                    e.getMessage()
            );
        }
    }
}

uploadBpmnPath 上传的目录,yml 中 通过 @Value("${uploadBpmnPath}") 引用

# bpmn 文件上传路径
upload:
  bpmn:
    path: G:/gu-pao/activiti7_workflow/src/main/resources/resources/bpmn/

上传的文件目录映射

@Configuration
public class PathMapping implements WebMvcConfigurer {

    @Value("${upload.bpmn.path}")
    private String uploadBpmnPath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 添加默认映射
        registry.addResourceHandler("/**").addResourceLocations("classpath:/resources/");
        // 添加 bpmn 路径映射
        registry.addResourceHandler("/bpmn/**")
                .addResourceLocations("file:"+uploadBpmnPath.replace("/","\\"));
    }
}

测试导入本地 bpmn,成功上传并回显

image.png

源码地址

https://gitee.com/jitashou18089237297/activiti-integrates-bpmnjs.git

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

推荐阅读更多精彩内容