react实现简易可视化编辑模板

1,295 阅读4分钟

项目用。 纯react+js实现可视化编辑的模板,可以修改字段,flex布局移动元素排序,absolute移动元素位置,控制元素显隐。

虽然有限,但也够用。

2.gif

变量

export default class Basic extends Component {
  state = {
    style: {
    	 // logo
      reportLogoStyle: {
        position: "absolute",
        left: '9px',
        top: '19px',
        height: "68px",
        width: "",
      },
    }

看起来是对象,实际发给后端存储的是style 的 json字符串,前端获取后端数据需要json格式化转成对象,这样后端数据库只占一个变量。

拖动元素

//拖动实时更新state里的style数据,元素渲染时实时用的style 
dragComponentFun = function () {
    // departmentNameStyle 科室名
    // reportLogoStyle logo
    // let dragArr = ['reportLogoStyle']
    let dragArr = ['reportLogoStyle', 'hospitalNameStyle', 'reportNameStyle', 'departmentNameStyle']
    let that = this
    dragArr.forEach(name => {
      // console.log(name)
        //鼠标点击
      document.getElementById(name).onpointerdown = function (e) {
        // 点击开始的坐标
        let startX = e.clientX
        let startY = e.clientY
        let { style } = that.state;
        // 原本元素的坐标
        let left = style[name].left
        let top = style[name].top
        //去掉 px
        left = Number(left.substring(0, left.length - 2))
        top = Number(top.substring(0, top.length - 2))

        // document.getElementById(name).style.border = '1px dashed #666'
        // 移动的元素等于 原本元素的坐标+偏差的坐标
        window.onmousemove = function (e) {
          style[name].left = left + (e.pageX - startX) + 'px'
          style[name].top = top + (e.pageY - startY) + 'px'
          that.setState({ style })
        }

        window.onpointerup = function () {
          window.onmousemove = null
          window.onpointerup = null
          // document.getElementById(name).style.border = 'none'
        }
      }
    })
  }

const {style}= state
<div style={{ height: "160px" }} onDragStart={this.ondragstart}>
            <img
              id="reportLogoStyle"
              onClick={this.showDrawer}
              draggable="true"
              className='hoverBorder'
              style={{ ...style.reportLogoStyle, visibility: style.showHeader.includes(1) ? 'visible' : 'hidden' }}
              src={hospitalInfo?.hospital_logo}
              alt=""
            />

输入修改元素样式

ondragstart = (ev) => ev.preventDefault()
  // 头部
showDrawer = (e) => {
    let id = e.target.id;
    this.setState({ drawerVisible: true, selectEle: id });
  };
//修改样式
changeSingleStyle = (key, e) => {
    let { style, selectEle } = this.state;
    let newObj = style[selectEle];
    console.log(newObj);
    newObj[key] = e.target.value;
    style[selectEle] = newObj;
    this.setState({ style });
  };

return (
    //获取选中元素的样式
    const {selectEle} = this.state
    const ele = style[selectEle];
      <div
        style={{
          height: "100%",
          width: "100%",
          display: "flex",
          flexDirection: "column",
          justifyContent: "space-between",
        }}
      >
          //右上角的样式内容查看和修改
       {drawerVisible ? (
          <div
            style={{
              position: "fixed",
              right: 0,
              background: "white",
              paddingLeft: "5px",
              borderRadius: "10px",
            }}
          >
            <div
              style={{
                textAlign: "right",
                fontSize: "14px",
                color: "blue",
                cursor: "pointer",
              }}
            >
              <span onClick={this.save} style={{ marginRight: "5px" }}>
                保存
              </span>
              <span onClick={this.onClose} style={{ marginRight: "5px" }}>
                关闭
              </span>
            </div>
//遍历样式显示
            {Object.keys(ele).map((i) => (
              <p key={selectEle + i} style={{ display: "flex" }}>
                {i}:
                <Input
                  defaultValue={ele[i]}
                  onChange={this.changeSingleStyle.bind(this, i)}
                />
              </p>
            ))}
          </div>
        ) : null}
        <div>
          <div style={{ height: "160px" }} onDragStart={this.ondragstart}>
            <img
              id="reportLogoStyle"
              onClick={this.showDrawer}
              draggable="true"
              className='hoverBorder'
              style={{ ...style.reportLogoStyle, visibility: style.showHeader.includes(1) ? 'visible' : 'hidden' }}
              src={hospitalInfo?.hospital_logo}
              alt=""
            />

拖动元素改位置

通过ondragstart监听要拖动的元素

通过ondragenter监听会被插入的元素

通过计算两者元素的位置关系,得出元素应该怎么插。

  • 渲染
style:{
    patientBasicList: [{ key: 'name', value: '姓名' }, { key: 'gender', value: '性别' }, { key: 'age', value: '年龄' }, { key: 'outpatient_id', value: '门诊号' }, { key: 'mobile', value: '联系方法' },
      { key: 'receive_time', value: '接收日期' },
      { key: 'origin_department_name', value: '送检科室' }, { key: 'origin_doctor_name', value: '送检医生' }, { key: 'slide_type', value: '标本类型' }],
}

<div
            className='reportBasic'
            style={{
              fontSize: style.patientBasicFontSize,
              display: "flex",
              flexWrap: "wrap",
              borderBottom: "1px solid black",
              // paddingBottom: "3px",
            }}
            onDragOver={this.ondragstart}
          >
            {style?.patientBasicList?.map((i, d) => {
              return (
                <div key={i.key + d} className='hoverBorder' onClick={this.showBasicDrawer} draggable='true' style={{
                  marginTop: "3px",
                  width: style.patientBasicWidth,
                  height: style.patientBasicHeight,
                }}>
                  <div style={{ width: "40%" }} >{i.value}:</div>
                </div>
              )
            })}

          </div>
  • 事件
componentDidMount() {
    this.dragSortComponentFun()
}

// 基础信息的拖拽
  dragSortComponentFun = (type) => {
    const lis = document.querySelectorAll(".reportBasic>div");
    // console.log('lis', lis)
    for (let i = 0; i < lis.length; i++) {
      // lis[i].setAttribute("draggable", true);
      lis[i].ondragstart = (e) => {
          //获取触发拖的元素
        console.log('start', e.target)
        this.draggingElement = e.target
        // console.log(this.draggingElement)
      }
      lis[i].ondragenter = (event) => {
          //每个元素都要监听拖入
        let { style } = this.state
        // event.stopImmediatePropagation();
        console.log('ondragenter', this.draggingElement)
        console.log(event.target)
        //每次都要新计算,因为有可能已经换位了
//计算当前拖的元素的位置
        let draggingElementOrder = Array.from(this.draggingElement.parentElement.children).indexOf(this.draggingElement);
        let node = event.target;
//有可能拖到元素里面的元素,所以这里调整一下,也可以把子元素样式调整成不可选中user-select: none;
        if (node.className !== 'hoverBorder') {
          node = node.parentElement
        }
//得到当前元素要插入的位置
        // draggingElementPosition = draggingElement.getBoundingClientRect();
        const order = Array.from(node.parentElement.children).indexOf(node);
        //从大的序号移入到小的序号
//往前插
        if (draggingElementOrder > order) {
          // xiao 没有setState去更改,不知道后面会出什么意外
            //插入node前 ,至于原来拖动的元素会自动清除
          node.parentElement.insertBefore(this.draggingElement, node);
          // console.log('原数组',style.patientBasicList)
          // console.log('move',style.patientBasicList[draggingElementOrder])
         	//style里调整node对应位置。
            style.patientBasicList.splice(order, 0, style.patientBasicList[draggingElementOrder])
            style.patientBasicList.splice(draggingElementOrder + 1, 1)
          
          // console.log('现数组',style.patientBasicList)
        }
//往后挪
        //从小的序号移入到大的序号
        else {
          //节点不是最后一个
          if (node.nextElementSibling) {
            node.parentElement.insertBefore(this.draggingElement, node.nextElementSibling);
           
              style.patientBasicList.splice(order + 1, 0, style.patientBasicList[draggingElementOrder])
              style.patientBasicList.splice(draggingElementOrder, 1)
            
          }
          // 节点是最后一个了,不能再用insertBefore
          else {
            node.parentElement.appendChild(this.draggingElement);
        
              style.patientBasicList.push(style.patientBasicList[draggingElementOrder])
              style.patientBasicList.splice(draggingElementOrder, 1)
            
          }

          // console.log('style.patientBasicList',style.patientBasicList)
        }
      }

    }
  }

模板字段更改

style: {
    patientBasicList: [
        { key: "name", value: "姓名" },
        { key: "gender", value: "性别" },
        { key: "age", value: "年龄" },
        { key: "outpatient_id", value: "门诊号" },
        { key: "mobile", value: "联系方法" },
        { key: "receive_time", value: "接收日期" },
        { key: "origin_department_name", value: "送检科室" },
        { key: "origin_doctor_name", value: "送检医生" },
        { key: "slide_type", value: "标本类型" },
      ],
    
// 基础信息
  showBasicDrawer = (e) => {
      //不定时器关开一下 内容不刷新
    setTimeout(() => {
      this.onClose();
    });
    let node = e.target;
    if (node.className !== "hoverBorder") {
      node = node.parentElement;
    }
    const order = Array.from(node.parentElement.children).indexOf(node);
    setTimeout(() => {
      this.setState({ drawerBasicVisible: true, basicIndex: order });
    });
  };
 // 基础信息字段 更改
  changeBasicInfo = async (type, e) => {
    let { style, basicIndex } = this.state;
    // type : add delete value key
    if (type === "add") {
      style.patientBasicList.push({ key: "key", value: "新建" });
      await this.setState({ style });
      // 新增之后需要重新绑定一下拖拽事件,不加await会发现最后一个新增元素没有被绑定上,异步更新的原因,
    } else if (type === "delete") {
      this.setState({ drawerBasicVisible: false });
      style.patientBasicList.splice(basicIndex, 1);
      // style.patientBasicList.push({value:'新建',key:'key'})
      await this.setState({ style });
      // 新增之后需要重新绑定一下拖拽事件,不加await会发现最后一个新增元素没有被绑定上,异步更新的原因,
    } else {
      style.patientBasicList[basicIndex][type] = e.target.value;
      this.setState({ style });
    }
    // 更改后 需要重新绑定元素,重新监听拖拽,名字改后,dom元素也不同了,需要重新绑
    this.dragSortComponentFun();
  };
//修改内容的窗口
{drawerBasicVisible ? (
          <div
            style={{
              position: "fixed",
              left: 0,
              background: "white",
              paddingLeft: "5px",
              borderRadius: "10px",
            }}
          >
            <div
              style={{
                textAlign: "right",
                fontSize: "14px",
                color: "blue",
                cursor: "pointer",
              }}
            >
              <span onClick={this.save} style={{ marginRight: "5px" }}>
                保存
              </span>
              <span onClick={this.onClose} style={{ marginRight: "5px" }}>
                关闭
              </span>
            </div>
            <p>
              基础信息字体大小:
              <Input
                style={{ width: "30%" }}
                defaultValue={style.patientBasicFontSize}
                onChange={this.changeTotalStyle.bind(
                  this,
                  "patientBasicFontSize"
                )}
              ></Input>
            </p>
            <p>
              基础信息单位宽度:
              <Input
                style={{ width: "30%" }}
                defaultValue={style.patientBasicWidth}
                onChange={this.changeTotalStyle.bind(this, "patientBasicWidth")}
              ></Input>
            </p>
            <p>
              基础信息单位高度:
              <Input
                style={{ width: "30%" }}
                defaultValue={style.patientBasicHeight}
                onChange={this.changeTotalStyle.bind(
                  this,
                  "patientBasicHeight"
                )}
              ></Input>
            </p>
            <p>
              字段名:
              <Input
                style={{ width: "30%" }}
                defaultValue={
                  style.patientBasicList[this.state.basicIndex].value
                }
                onChange={this.changeBasicInfo.bind(this, "value")}
              ></Input>
            </p>
            <p>
              字段key:
              <Input
                style={{ width: "60%" }}
                defaultValue={style.patientBasicList[this.state.basicIndex].key}
                onChange={this.changeBasicInfo.bind(this, "key")}
              ></Input>
            </p>
            {/* <p>字段名 key</p> */}
            <Button
              type="primary"
              style={{ margin: "0 10px 10px 10px" }}
              onClick={() => this.changeBasicInfo("add")}
            >
              新增字段
            </Button>
            <Button
              type="primary"
              danger
              onClick={() => this.changeBasicInfo("delete")}
            >
              删除字段
            </Button>
//渲染的内容
{style?.patientBasicList?.map((i, d) => {
              return (
                <div
                  key={i.key + d}
                  className="hoverBorder"
                  onClick={this.showBasicDrawer}
                  draggable="true"
                  style={{
                    marginTop: "3px",
                    width: style.patientBasicWidth,
                    height: style.patientBasicHeight,
                  }}
                >
                  <div style={{ width: "40%" }}>{i.value}:</div>
                  {/* <div style={{ width: "60%" }}>
                  {schema?.patient_info[i.key] || schema?.case_info[i.key] ||  ""}
                </div> */}
                </div>
              );
            })}

a4布局

// 这里是报告模板编辑页面
import React, { Component } from "react";
export default class ReportPreview extends Component {
  state = {
    info: {}, //模板内容
    hospitalInfo: {},
  };
  onClose = (e) => {
      //防止点击穿透
    e.stopPropagation();
  };
  render() {
    const { hospitalInfo } = this.state;
    const { visible, close, info } = this.props;
    return (
      <div
        onClick={close}
        style={
          visible
            ? {
                position: "fixed",
                width: "100%",
                height: "100%",
                left: "0",
                top: "0",
                // top: "30px",
                background: "rgba(0, 0, 0, 0.3)",
                zIndex: 7,
                display: "block",
                overflow: "auto",
              }
            : { display: "none" }
        }
      >
        {/* 210*297mm */}
        <div
          style={{
            height: "279mm",
            width: "215mm",
            position: "absolute",
            left: "50%",
            marginLeft: "-105mm",
            // top: "66px",
            zIndex: "9999",
            background: "white",
            // padding: "80px 60px 20px 60px",
            /* boxShadow: 'rgb(51, 51, 51) 2px 2px 5px', */
            MozBoxShadow: "0px 0px 11px 2px #333333",
            WebkitBoxShadow: "0px 0px 11px 2px #333333",
            boxShadow: "0px 0px 11px 2px #333333",
          }}
          onClick={this.onClose}
          id="preview_report_m"
        >
          <div
            id="preview_report"
            style={{
              fontFamily: '"宋体"',
              overflow: "hidden",
              padding: "5mm 0 5mm 5mm",
              // marginRight: "5mm",
              fontSize: "17px",
              position: "relative",
              // width: "1030px",
              // height: "100%",
              // height: "1527px",
              width: "210mm",
              height: "279mm",
            }}
          >
            <Basic hospitalInfo={hospitalInfo} info={info} />
          </div>
        </div>
      </div>
    );
  }
}

模板渲染

//布局类似,值填入 schema?.[i.key],需要后端接口有提供这个字段
{style?.patientBasicList?.map((i, d) => {
              return (
                <div key={i.key + d} style={{
                  marginTop: "3px",
                  width: style.patientBasicWidth,
                  height: style.patientBasicHeight,
                  display: 'flex'
                }}>
                  <div style={{ width: "40%" }} >{i.value}:</div>
                  <div style={{ width: "60%" }}>
                    {schema?.[i.key] || ""}
                  </div>
                </div>
              )
            })}

导出pdf

import html2pdf from "html2pdf.js";
exportPDF = (name) => {
    // 要导出的dom节点
    const element = document.getElementById("preview_report");
    // 导出配置
    const opt = {
      margin: 0,
      filename: `${name}.pdf`,
      image: { type: "jpeg", quality: 0.98 }, // 导出的图片质量和格式
      html2canvas: { scale: 1, useCORS: true }, // useCORS很重要,解决文档中图片跨域问题
      jsPDF: { unit: "in", format: "letter", orientation: "portrait" },
    };
    if (element) {
      html2pdf().set(opt).from(element).save(); // 导出
    }
  };

打印

import ReactToPrint from "react-to-print";
<ReactToPrint
              trigger={() => (
                <button
                  id="printBtn"
                >
                  打印
                </button>
              )}
              content={() => this.componentRef}
            />
            
            
<div
            ref={(el) => (this.componentRef = el)}
            id="preview_report"
          >
            <Basic schema={schema} />
          </div>