项目用。 纯react+js实现可视化编辑的模板,可以修改字段,flex布局移动元素排序,absolute移动元素位置,控制元素显隐。
虽然有限,但也够用。
变量
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>