自定义文书思路
遇到一个需求:实现自定义文书,要求如下:
- 支持分库情况
- 支持留空部分自动填充
- 解决前端打印问题
- 支持电子签名功能
名词分析:
1.什么是自定义文书;自定义文书应该是类似Word,或者富文本类型的可进行样式编辑的功能。类似如下图所示的内容:

可以编辑样式,可以选择留空以及数据填充部分。且无编程经验人员亦可实现操作。
2.什么是电子签名?
《电子签名法》规定:可靠的电子签名与手写签名或者盖章具有同等的法律效力。“可靠的电子签名”要求:
(一)电子签名制作数据用于电子签名时,属于电子签名人专有;
(二)签署时电子签名制作数据仅由电子签名人控制;
(三)签署后对电子签名的任何改动能够被发现;
(四)签署后对数据电文内容和形式的任何改动能够被发现。
这个有点像啥....私钥和公钥的含义,任何改动能够被发现,电子签名人专有。
不过这个功能一般的场景,都是签个字就完事了。是否需要这么复杂呢?这点有待商榷。
问题分析:
介绍完了名词含义,就看下实现中可能遇到的问题有哪些?
1.前段打印问题
这边的打印指那种含义下的打印?
个人理解打印有两层含义,一种是通过标识符或者调用游览器预留的API进行打印
function doPrint() {
bodyHtml = window.document.body.innerHTML;
sPrintStr = "<!--startprint-->"; //开始打印标识字符串有17个字符
ePrintStr = "<!--endprint-->"; //结束打印标识字符串
printHtml = bodyHtml.substr(bodyHtml.indexOf(sPrintStr) + 17); //从开始打印标识之后的内容
printHtml = printHtml.substring(0, printHtml.indexOf(ePrintStr)); //截取开始标识和结束标识之间的内容
window.document.body.innerHTML = printHtml; //把需要打印的指定内容赋给body.innerHTML
window.print(); //调用浏览器的打印功能打印指定区域
window.document.body.innerHTML = bodyHtml;//重新给页面内容赋值;
}
/**
* 打印
* @command print
* @method execCommand
* @param { String } cmd 命令字符串
* @example
* ```javascript
* editor.execCommand( 'print' );
* ```
*/
UE.commands['print'] = {
execCommand: function() {
this.window.print();
},
notNeedUndo: 1
};
实现效果如下:

一种则为广义上的打印,将渲染后的页面,保存为图片,如dom-to-image这插件进行,举例代码如下:
<script type="text/javascript">
$(document).ready(function(){
// canvas生成图片
$("#btn").on("click", function () {
var getPixelRatio = function (context) { // 获取设备的PixelRatio
var backingStore = context.backingStorePixelRatio ||
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 0.5;
return (window.devicePixelRatio || 0.5) / backingStore;
};
//生成的图片名称
var imgName = "cs.jpg";
var shareContent = document.getElementById("imgDiv");
var width = shareContent.offsetWidth;
var height = shareContent.offsetHeight;
var canvas = document.createElement("canvas");
var context = canvas.getContext('2d');
var scale = getPixelRatio(context); //将canvas的容器扩大PixelRatio倍,再将画布缩放,将图像放大PixelRatio倍。
canvas.width = width * scale;
canvas.height = height * scale;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
context.scale(scale, scale);
var opts = {
scale: scale,
canvas: canvas,
width: width,
height: height,
dpi: window.devicePixelRatio
};
html2canvas(shareContent, opts).then(function (canvas) {
context.imageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
var dataUrl = canvas.toDataURL('image/jpeg', 1.0);
dataURIToBlob(imgName, dataUrl, callback);
});
});
})
// edited from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Polyfill
var dataURIToBlob = function (imgName, dataURI, callback) {
var binStr = atob(dataURI.split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
callback(imgName, new Blob([arr]));
}
var callback = function (imgName, blob) {
var triggerDownload = $("<a>").attr("href", URL.createObjectURL(blob)).attr("download", imgName).appendTo("body").on("click", function () {
if (navigator.msSaveBlob) {
return navigator.msSaveBlob(blob, imgName);
}
});
triggerDownload[0].click();
triggerDownload.remove();
};
</script>
前端实现打印功能是没有问题的,前端打印是否会带来问题?

这个应该通过设置高度可以进行修复
打印默认显示页眉和页脚;打印时表格的边框没有,因为chrome打印默认设置都是不显示背景颜色和图片的,需要自己手动设置显示;element ui table使用boder模式的时候线条不明显,表头线条会多出来;boder线框只显示一部分,呈现断开。网上查出了如下的问题,但基本都有解决方案,一般这些都是样式上的原因导致的可能大一点,通过样式上进行修复应该是可行的。
那这边就假定前端可能可以实现打印功能,看下后端如何处理
后端数据集
首先不管打印问题交给前端还是后端处理都需要先进行数据集的功能
后端单机情况下实现数据集是很简单的实现,数据库帮助做查询语句的持久化,程序帮助进行SQL校验,确保SQL安全,无SQL注入等不可控风险。
实现逻辑如下:
SQL前置解析器
public class SqlXmlUtil {
public static String SqlXmlParse(String xmlSql, Map<String,String> paramMap) throws Exception{
if (StringUtils.isNotEmpty(xmlSql)) {
xmlSql=xmlSql.trim();
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
String sql=""; //sql语句
boolean sqlJlflag=true; //是否记录sql
String labelName=""; //标签名称
boolean labelJlFlag=false; //是否记录标签
String tiaoJianStr=""; //条件字符串
boolean tjJlFlag=false; //是否记录条件
String tjSql=""; //条件sql语句
boolean tjSqlJlFlag=false; //是否记录sql条件语句
String endLabelName=""; //结束标签名称
boolean endLabelJlFlag=false; //是否记录结束标签名称
String andOrSql=""; //and或or sql语句
boolean andOrSqlJlFlag=false; //是否记录and或or sql语句
char prevZm=' '; //上一个字符串
//遍历字符串
for (int i=0;i<xmlSql.length();i++){
char zm=xmlSql.charAt(i); //获取每一个字符
//1、找到第一个以<开头之前的字符串记录下来当sql
//2、如果以是<则记录接下来的字母直到找到空格,这个字符串则是标签名称
//3、如果标签是where则什么都不做。如果是if则判断test中的条件是否成立,成立则追加sql语句
//判断标签记录标识、条件记录标识、结束标签记录标识都为false
if(!sqlJlflag && !labelJlFlag && !tjJlFlag && !endLabelJlFlag){
if(zm=='<' || i==xmlSql.length()-1){
if(andOrSql.indexOf("and")!=-1 || andOrSql.indexOf("or")!=-1 ||
andOrSql.indexOf("AND")!=-1 || andOrSql.indexOf("OR")!=-1) {
andOrSqlJlFlag = false; //结束记录and、or语句
if(i==xmlSql.length()-1){ //加上最后一个字符
andOrSql+=zm;
}
String tjSqlCl = jxGjz(andOrSql, paramMap);
sql += tjSqlCl + " ";
// System.out.println(andOrSql);
andOrSql = "";
}
}else{
andOrSqlJlFlag=true; //开始记录and、or语句
}
}
//判断出现'<' 和 记录条件标识为false,说明不是结束标签的'<'
if(zm=='<' && !tjSqlJlFlag){
if(sqlJlflag && sql.indexOf("where")==-1 && sql.indexOf("WHERE")==-1){ //记录sql标识为true,避免后面循环循环追加
sql+=" where 1=1 ";
}
sqlJlflag=false; //记录sql标识改为false
labelJlFlag=true; //记录标签标识改为true
}
//判断出现空格 和 记录标签标识为true,说明记录标签结束
if((zm==' ' && labelJlFlag) || ((labelName.indexOf("where")!=-1 || labelName.indexOf("WHERE")!=-1) && labelJlFlag)){
labelJlFlag=false; //记录标签标识改为false
if(labelName.trim().indexOf("if") != -1 || labelName.trim().indexOf("IF") != -1) { //是if标签
tjJlFlag = true; //记录条件标识改为true
labelName="if";
}else{ //说明是where标签
labelName=""; //清空标签名称
}
}
//判断出现'>' 和记录条件标识为true,说明记录条件结束
if(zm=='>' && tjJlFlag){
tjJlFlag=false; //记录条件标识改为false
tjSqlJlFlag=true; //记录条件sql语句标识改为true
}
//判断出现'<' 和 记录条件sql标识为true,说明记录条件sql结束
if(zm=='<' && tjSqlJlFlag){
tjSqlJlFlag=false; //记录条件sql语句标识改为false
endLabelJlFlag=true; //记录结束标签标识改为true
}
//判断出现'>' 和 记录结束标签标识为true,说明记录结束标签结束
if(zm=='>' && endLabelJlFlag){
endLabelJlFlag=false; //记录结束标签标识改为false
//判断是否追加条件
String filedValue="";
boolean isAppendFlag=judgeAppendSql(tiaoJianStr,paramMap,engine);
if(isAppendFlag){ //追加条件
// String value=paramMap.get("resultValue!@#$%"); //需要替换的值
//处理条件sql
String tjSqlCl=jxGjz(tjSql,paramMap);
sql+=tjSqlCl+" ";
}
//重置变量
labelName=""; //标签名称
labelJlFlag=false; //是否记录标签
tiaoJianStr=""; //条件字符串
tjJlFlag=false; //是否记录条件
tjSql=""; //条件sql语句
tjSqlJlFlag=false; //是否记录sql条件语句
endLabelName=""; //结束标签名称
endLabelJlFlag=false; //是否记录结束标签名称
}
//记录sql
if(sqlJlflag){
sql+=zm;
continue;
}
//记录条件标签名称
if(labelJlFlag){
labelName+=zm;
continue;
}
//记录条件
if(tjJlFlag){
tiaoJianStr+=zm;
continue;
}
//记录条件sql语句
if(tjSqlJlFlag && zm!='>'){
tjSql+=zm;
continue;
}
//记录结束标签
if(endLabelJlFlag && zm != '<' && zm != '/'){
endLabelName+=zm;
continue;
}
//and或or sql语句
if(andOrSqlJlFlag && zm!='<' && zm!='>' && zm!='/'){
andOrSql+=zm;
}
// prevZm=zm; //设置上一个字符串
}
return sql.trim();
}
return null;
}
/**
* 解析占位符
* @param tjSql
* @param paramMap
* @return
*/
public static String jxGjz(String tjSql,Map<String,String> paramMap){
String tjSqlCl="";
boolean isStrFlag=false; //是否拼接单引号
boolean isGjsFlag=false; //是否开始解析关键字
String gjz="";
boolean isEndFlag=false; //是否是结尾值
String endValue=""; //结尾值
for (int j=0;j<tjSql.length();j++){
char zm2=tjSql.charAt(j); //获取每一个字符
if(zm2=='#'){
isGjsFlag=true;
}else if(zm2=='$'){
isStrFlag=true;
isGjsFlag=true;
}
if(!isEndFlag) { //不是结尾值
if (isGjsFlag) {
gjz += zm2;
} else {
tjSqlCl += zm2;
}
}else{
endValue+=zm2;
}
if(zm2=='}'){
isEndFlag=true;
}
}
gjz=gjz.replace("#","").replace("$","").replace("{","").replace("}","").trim();
/*if(isStrFlag){ //拼接单引号
value="'"+value+"'";
}*/
String value=paramMap.get(gjz);
if(value != null) {
tjSqlCl +="'"+value+"'";
}
return tjSqlCl+endValue;
}
/**
* 判断是否追加sql
* @return
*/
private static boolean judgeAppendSql(String tiaoJianStr, Map<String,String> paramMap,ScriptEngine engine){
if(StringUtils.isNotEmpty(tiaoJianStr)) {
String filedName = ""; //条件字段
String value = ""; //值
//处理条件逻辑
// String numParent="^[0-9]*$";
String ysf = ""; //运算符
if (tiaoJianStr.indexOf("==") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf("==")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf("==") + 2, tiaoJianStr.lastIndexOf('"')).trim();
ysf = "==";
} else if (tiaoJianStr.indexOf("!=") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf("!=")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf("!=") + 2, tiaoJianStr.lastIndexOf('"')).trim();
ysf = "!=";
} else if (tiaoJianStr.indexOf(">") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf(">")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf(">") + 1, tiaoJianStr.lastIndexOf('"')).trim();
ysf = ">";
} else if (tiaoJianStr.indexOf(">=") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf(">=")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf(">=") + 2, tiaoJianStr.lastIndexOf('"')).trim();
ysf = ">=";
} else if (tiaoJianStr.indexOf("<") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf("<")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf("<") + 1, tiaoJianStr.lastIndexOf('"')).trim();
ysf = "<";
} else if (tiaoJianStr.indexOf("<=") != -1) {
filedName = tiaoJianStr.substring(tiaoJianStr.indexOf('"') + 1, tiaoJianStr.indexOf("<=")).trim();
value = tiaoJianStr.substring(tiaoJianStr.indexOf("<=") + 2, tiaoJianStr.lastIndexOf('"')).trim();
ysf = "<=";
}
if (StringUtils.isNotEmpty(filedName)) {
String paramValue = paramMap.get(filedName); //参数值
/*if(StringUtils.isEmpty(paramValue)) {
return false;
}*/
String str = paramValue + ysf + value; //条件
if(isInteger(filedName)) {
str=filedName + ysf + value;
}
Object result = null;
try {
//执行判断条件是否成立
result = engine.eval(str);
} catch (ScriptException e) {
e.printStackTrace();
}
// System.out.println("结果类型:" + result.getClass().getName() + ",计算结果:" + result);
if (result != null && (boolean) result == true) { //成立拼接sql
// paramMap.put("resultValue!@#$%",paramValue);
return true;
}
}
}
return false;
}
public static boolean isInteger(String str) {
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
return pattern.matcher(str).matches();
}
public static void main(String[] args) throws Exception{
Map<String,String> param=new HashMap<> ();
// param.put("编号","1");
// param.put("名称","张三");
// String xmlSql="SELECT jianKangHao 健康号,zhenDuanXinXi 诊断信息 FROM bingRenZhenDuanXinXi<WHERE> <IF test=\"jianKangHao != null\">AND jianKangHao = #{jianKangHao}</IF> AND zhenDuanShuXin = '入院诊断'</WHERE>";
// String xmlSql="SELECT 编号,名称 FROM 测试 <WHERE>and 名称=#{名称} <IF test=\"编号 != null \">and 编号=#{编号}</IF></WHERE>";
// String xmlSql="SELECT jianKangHao 健康号,zhenDuanXinXi 诊断信息 FROM bingRenZhenDuanXinXi<WHERE><IF test=\"1 == 1\">AND zhenDuanShuXin = '入院诊断'</IF> </WHERE>";
String xmlSql="SELECT jianKangHao 健康号,zhenDuanXinXi 诊断信息 FROM bingRenZhenDuanXinXi<WHERE><IF test=\"1 == 1\">AND zhenDuanShuXin = '入院诊断'</IF> AND zhenDuanShuXin = '入院诊断</WHERE>";
String sql=SqlXmlParse(xmlSql,param);
System.out.println(sql);
// System.out.println(xmlSql.substring(xmlSql.indexOf("!=")+2,xmlSql.lastIndexOf('"')));
}
}
SQL前置安全校验
public class SQLFilter {
/**
* SQL注入过滤
* @param str 待验证的字符串
*/
public static String sqlInject(String str){
if(StringUtils.isBlank(str)){
return null;
}
//去掉'|"|;|\字符
str = StringUtils.replace(str, "'", "");
str = StringUtils.replace(str, "\"", "");
str = StringUtils.replace(str, ";", "");
str = StringUtils.replace(str, "\\", "");
//转换成小写
str = str.toLowerCase();
//非法字符
String[] keywords = {"master", "truncate", "insert", "select", "delete", "update", "declare", "alter", "drop"};
//判断是否包含非法字符
for(String keyword : keywords){
if(str.contains (keyword)){
throw new EXException ("包含非法字符");
}
}
return str;
}
}
然后保存下的SQL只需要在填充后执行即可
<select id="dosql" resultType="java.util.LinkedHashMap">
${documentSql}
</select>
然后考虑进不同数据库之间 分库情况,目前的业务仅仅为查询,所以是一个读场景,则无需关注分库情况下的事物,那么就不用引入复杂的MyCat orSharing-JDBC的真正的代理层情况
完全可以通过Java内部容器进行库,字段,数据库之间的映射关系进行管理。

假设是在分布式的情况下,如何动态感知出每个数据库的情况 (eq.地址,连接账号,密码,数据表,各个表的字段)
在中心化的分布式情况下——————>有注册中心和配置中心,而非Mesh体系
通过配置中心则可以感知到各个配置文件的内容————>得到对应数据库地址,账号,密码——————>表结构以及字段
如果是非中心化情况下,或者类似簇结构的情况下,则如果有数据代理层,如MyCat统一管理,则可以在Mycat处获取到。
这些的前提就是数据库相关配置需要被某一个处进行集中化管理。或者只能让各个服务开口子,预留。这就很扯淡了。
那么后台数据出入 这一层不难处理。
后端实现模板
Java操作Word,通过实现Word模板后填充数据,直接操作Word不现实。
如果使用PageOffice是可以实现在线编辑的效果,缺点:客户端必须安装pageoffice,而且还后期需要收费
Pom文件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
<!-- 添加Sqlite依赖(可选:如果不需要使用印章功能的话,不需要添加此依赖)-->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.7.2</version>
</dependency>
<!-- 添加PageOffice依赖(必须) -->
<dependency>
<groupId>com.zhuozhengsoft</groupId>
<artifactId>pageoffice</artifactId>
<version>4.6.0.3</version>
</dependency>

打开后有个类似GUI的编辑器,但是需要付费...否则无法使用
另外一种就是使用freemarker
public class Test { public static void main(String[] args) throws IOException, TemplateException { //1.创建配置对象 Configuration configuration = new Configuration(Configuration.getVersion()); //2.设置模板文件所在的路径 configuration.setDirectoryForTemplateLoading(new File("D:\\idea_workspace\\freemarker-demo\\src\\main\\resources")); //3.设置模板文件使用的字符集。一般就是 utf-8 configuration.setDefaultEncoding("utf-8"); //4.加载一个模板,创建一个模板对象 Template template = configuration.getTemplate("test.ftl"); //5.创建一个模板使用的数据集,可以是 pojo 也可以是 map。一般是 Map HashMap map = new HashMap(); map.put("name", "木丁西"); map.put("message", "这是freemarker入门小demo"); map.put("success", true); Map goods1=new HashMap(); goods1.put("name", "苹果"); goods1.put("price", 5.8); Map goods2=new HashMap(); goods2.put("name", "香蕉"); goods2.put("price", 2.5); Map goods3= new HashMap(); goods3.put("name", "橘子"); goods3.put("price", 3.2); List goodsList=new ArrayList(); goodsList.add(goods1); goodsList.add(goods2); goodsList.add(goods3); map.put("goodsList", goodsList); //6.创建一个 Writer 对象,一般创建一 FileWriter 对象,指定生成的文件名 FileWriter fileWriter = new FileWriter(new File("")); //7.调用模板对象的 process 方法输出文件 template.process(map, fileWriter); //8.关闭流 fileWriter.close(); } }
通过调用xxxx.ftl进行生成
<!DOCTYPE html>
<html>
<head>
<title>报告</title>
<meta charset="UTF-8">
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.7/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.8.0/echarts.min.js"></script>
<style>
.box{
width: 1200px;
margin: 100px auto 0;
min-height: 500px;
background: rgba(0, 0, 0, .3);
display: flex;
flex-direction: row;
padding: 0;
}
.box li{
list-style: none;
flex: 1;
}
.box li:first-child{
margin-right: 50px;
}
#rose, #rtch{
width: 575px;
height: 575px;
}
</style>
</head>
<body>
<div>
<div id="app"></div>
</div>
</body>
<script type="text/x-template" id='script'>
<ul class="box">
<li id="rose"></li>
<li id="rtch" ref="rtch">
<canvas ref="rtchCanvas"></canvas>
</li>
</ul>
</script>
<script type="text/javascript">
window.onload = () => {
let vm = new Vue({
el: '#app',
template: '#script',
mounted() {
// rose
let roseChart = echarts.init(document.getElementById('rose'));
roseChart.setOption(this.optionsRose, true);
// rtch
this.w = this.$refs.rtch ? this.$refs.rtch.offsetWidth : '';
this.h = this.$refs.rtch ? this.$refs.rtch.offsetHeight : '';
this.createTable();
const v = ${data2};
this.filterDot(v);
},
data: {
optionsRose: {
"legend":{
"show":false
},
"tooltip":{
"trigger":"item",
"formatter":"{b}:{c}%"
},
"calculable":true,
"polar":{
"center":[
"50%",
"50%"],
"radius":"65%"
},
"radiusAxis":{
"min":0,
"max":100,
"axisLine":{
"show":true
},
"axisTick":{
"show":false
},
"axisLabel":{
"show":false
},
"splitLine":{
"lineStyle":{
"type":"solid",
"color":"#C9CFD4"
}
}
},
"angleAxis":{
"type":"category",
"splitLine":{
"show":true,
"lineStyle":{
"width":1,
"type":"dashed",
"color":"#C9CFD4"
}
},
"axisLine":{
"lineStyle":{
"color":"rgba(255,255,255,1)"
}
},
"axisTick":{
"show":false
},
"axisLabel":{
"show":false
},
"data":[
1,
2,
3,
4,
5,
6,
7,
8,
9]
},
"series":[
{
"name":"课堂行为",
"type":"pie",
"radius":[
"12%",
"65%"],
"center":[
"50%",
"50%"],
"roseType":"area",
"label":{
"show":true,
"fontSize":12,
"color":"#232323",
"formatter":"{b}:\n{c}%"
},
"labelLine":{
"show":true
},
"data":${data}
}]
},
ctx: null,
x: '',
y: '',
w: 0,
h: 0,
type: {
tableColor: '#000',
fontColor: '#232323'
}
},
computed: {
TYPE() {
let {titleSize = '16px', coordSize = '12px', tableColor = 'rgba(255,255,255,0.5)', fontColor = '#d6d6d6', lineWidth = 1, TriangleLinewidth = 2, TriangleSize = '14px', TriangleColor = '#2DABFF'} = this.type;
return {titleSize, coordSize, tableColor, fontColor, lineWidth, TriangleLinewidth, TriangleSize, TriangleColor};
}
},
methods: {
drawCircle(x, y, fillStyle, r = 5) { // 绘制圆形rtch
let ctx = this.ctx;
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.closePath();
ctx.fill();
},
drawPolygon(conf) { // 绘制多边形rtch
let ctx = this.ctx;
let x = (conf && conf.x) || 0; // 中心点x坐标
let y = (conf && conf.y) || 0; // 中心点y坐标
let num = (conf && conf.num) || 3; // 图形边的个数
let r = (conf && conf.r) || 10; // 图形的半径
let width = (conf && conf.width) || 5;
let strokeStyle = conf && conf.strokeStyle;
let fillStyle = conf && conf.fillStyle;
// 开始路径
ctx.beginPath();
let startX = x + r * Math.cos(2 * Math.PI * 0 / num);
let startY = y + r * Math.sin(2 * Math.PI * 0 / num);
ctx.moveTo(startX, startY);
for (let i = 1; i <= num; i++) {
let newX = x + r * Math.cos(2 * Math.PI * i / num);
let newY = y + r * Math.sin(2 * Math.PI * i / num);
ctx.lineTo(newX, newY);
}
ctx.closePath();
// 路径闭合
if (strokeStyle) {
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = width;
ctx.lineJoin = 'round';
ctx.stroke();
}
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
},
filterDot(v) {
if (!v.coord) {
return;
}
let x = v.coord[0] * (this.w - 3 * this.x) + 2 * this.x;
let y = (1 - v.coord[1]) * (this.h - 3 * this.y) + this.y;
if (v.num) {
let o = {
x: x,
y: y,
num: v.num || 3,
r: v.r || 10,
fillStyle: v.fillStyle || '#2DABFF'
};
this.drawPolygon(o);
} else {
let fillStyle = v.fillStyle || '#2DABFF';
let r = v.r || 5;
this.drawCircle(x, y, fillStyle, r);
}
},
createTable() {
this.$refs.rtchCanvas.width = this.w;
this.$refs.rtchCanvas.height = this.h;
let x = this.x = this.w / 13; // 每格宽度
let y = this.y = this.h / 13; // 每格高度
let ctx = this.ctx = this.$refs.rtchCanvas.getContext('2d');
ctx.clearRect(0, 0, this.w, this.h);
ctx.beginPath();
ctx.lineWidth = this.TYPE.lineWidth; // 设置线宽
ctx.fillStyle = this.TYPE.fontColor; // 文字颜色
ctx.strokeStyle = this.TYPE.tableColor; // 设置表格线的颜色
let startX = 0; // x起点
let startY = 0; // y起点
let k = 1; // y坐标轴
for (let i = 0; i < 14; i++) {
if (i > 0 && i < 12) {
// 横轴
startY = y * i;
ctx.moveTo(2 * x, startY);
ctx.lineTo(this.w - x, startY);
}
if (i === 0) { // y坐标轴名称
ctx.font = this.TYPE.titleSize +` Arial normal`;
ctx.fillText('CH', x + 10, 16);
}
for (let j = 0; j < 14; j++) {
if (j > 1 && j < 12) {
if (i > 0 && i < 12) {
// 纵轴
startX = x * (i + 1);
ctx.moveTo(startX, y * (j - 1));
ctx.lineTo(startX, y * j);
}
}
if (i === 0 && j > 1 && j < 13) { // y坐标序号
ctx.font = this.TYPE.coordSize +` Arial normal`;
ctx.fillText(j === 12 ? 0 : k.toFixed(1), x - 4, y * (j - 1) + 6);
k -= 0.1;
}
if (i > 1 && i < 12 && j === 12) { // x坐标序号
ctx.font = this.TYPE.coordSize +` Arial normal`;
ctx.fillText(i > 10 ? '1.0' : '0.' + (i - 1), x * i + x / 2, this.h - 5 - y);
}
if (i === 12 && j === 12) { // x坐标轴名称
ctx.font =this.TYPE.titleSize+ ` Arial normal`;
ctx.fillText('RT', this.w - 18, this.h - 3 * y / 2);
}
}
}
//
ctx.stroke();
ctx.closePath(); // 表格绘制完毕
this.createTriangle(x, y);
},
createTriangle(x, y) {
let ctx = this.ctx;
ctx.beginPath(); // 开始绘制三角形
ctx.strokeStyle = this.TYPE.TriangleColor;
ctx.lineWidth = this.TYPE.TriangleLinewidth;
ctx.moveTo(2 * x, this.h - 2 * y);
ctx.lineTo(7 * x, y);
ctx.lineTo(this.w - x, this.h - 2 * y);
ctx.lineTo(2 * x, this.h - 2 * y);
// 三角形内的分割线
ctx.moveTo(4 * x, this.h - 6 * y);
ctx.lineTo(10 * x, this.h - 6 * y);
//
ctx.moveTo(5 * x, this.h - 8 * y);
ctx.lineTo(5 * x, this.h - 2 * y);
//
ctx.moveTo(9 * x, this.h - 8 * y);
ctx.lineTo(9 * x, this.h - 2 * y);
ctx.stroke();
ctx.closePath();
ctx.beginPath();
// 绘制文字
ctx.font = this.TYPE.TriangleSize +` Arial normal`;
ctx.fillText('对话型', 7 * x - 48 / 2, this.h - 8 * y); // 48/2 为字体宽度
ctx.fillText('混合型', 7 * x - 48 / 2, this.h - 4 * y); // 48/2 为字体宽度
ctx.fillText('练习型', 3 * x, this.h - 3 * y);
ctx.fillText('讲授型', 9 * x + 5, this.h - 3 * y);
ctx.stroke();
ctx.closePath();
},
}
});
}
</script>
</html>
生成后的效果:

生成确实是可以生成,就是没有了自定义的效果,每一个表单都需要一个对饮的模板进行生成,工作量相对比较大
freemarker大致原理是:将页面中所需要的样式放入FreeMarker文件中,然后将页面所需要的数据动态绑定,并放入Map中,通过调用FreeMarker模板文件解析类process()方法完成静态页面的生成。这好像和前端写html一样,不同就是省去了网络传输这一过程,后端对于数据的绑定处理更加方便。

浙公网安备 33010602011771号