RM: 基于页面结构化数据生成报表,一键导出图片,生成定制图表 文末有效果图 , 开放部分代码

背景


      开发这个工具是因为一句抱怨

       故事是这样的,我们公司是一个非常重视员工健康的公司,一年前老董说让HR(后面改为ZT)督促员工多多运动,可持续地位公司创造价值.并拿出了一部预算来奖励那些积极运动的人.于是我们公司一百多人,被ZT分为三组,一组大约37人,然后每月统一下每组的运动积分, 男生8公里3分,女生6公里三分, 未来督促大家积极参与集体运动,HR有说, 每组的总积分为: 总分数 * 总参与人数 / 总人数

    具体的统计方式是这样的,每个人跑了步后,只需将运动截图通过公司办公软件发给ZT,然后ZT会在月末的时候,将全公司一百多人的聊天记录扒一边,找到发送给她的运动截图.统计到excel里,这里还要查这个人是那一组的,应该给多少积分.过程极其繁杂,并且容易出错.需要收集的信息也很多.因为算运动积分这事只有她自己,经常导致我们到了下个月十号左右才出来运动结果,因为如果遗漏了谁的记录,需要ZT再去补充,计算.反正很麻烦. 那么麻烦,妹子当然有怨言啊,这是一件吃力不讨好的事情,算不准确,还会被人质疑能力有问题.反正我就被她漏掉过一次运动积分,还有一次日期记错了. 妹子在运动群里的抱怨,

大家都看到了,作为开发人员的大家,都没当一回事,有的还调侃了一下. 这个时候我略作思考就提出了很合理,高效的方案.

 

这个方案将收集信息的操作,下发到个人,而管理员只担任一个审核积分,跑步截图和计算积分的角色.并最终输出报表.

说完这句话我就后悔了, 因为我想到了一个更加优秀的方案来解放生产力, 在这里我也想问一下,屏幕前的你,如果把这个计算运动积分的事情交给你,你要怎么做? 好好想想.....可以先别往下看

 

 

首先我们分析一下, 在Confluence 我们可以创建一个运动模板,添加一个表格, 可以设置几列,这个表格可以无限地往下加,每个人运动了,都可以在上面添加一条记录 这个表格就长这个样子

 

标题,内容就是一个表格

想到表格我们就知道了,表格属于一种结构化的数据,  所谓结构化数据 就是 结构化数据也称作行数据,是由二维表结构来逻辑表达和实现的数据,严格地遵循数据格式与长度规范,主要通过关系型数据库进行存储和管理(来自百度百科)。

大致意思就是,表头明确定义了这一列是什么数据,第一列是名字,你就不能把运动积分填到第一列. 结构化的数据安装一定的规则排列,填写. 而处理结构化数据正是软件程序的拿手好菜....于是用软件来处理这些运动记录准没错. 所以我一看到这列表我就立即想到了可以使用TM写一个工具脚本来分组,计算积分,制作报表. Nice 说干就干....这对我来说轻车熟路. 因为我就是为了提高社会生产力而来,去吧 皮卡丘.....

技术方案

方向有了,就是制定思路

  1. 使用脚本拿到结构化数据
  2. 处理数据,积分相加,人员去重,分组
  3. 创建展示页面
  4. 使用html2canvas一键下载报表图片

 

      思路有了,那就给它起一个名字吧, RM怎么样? report mark, report mother 报表制作, 报表之母  简洁明了.高大上.强, 我起名字的能力真是越来越强了...哈哈哈.... 中文释义为:基于页面结构化数据一键生成报表

 

经过下班后的几个晚上的艰苦调试,最后完成了这个不到300行代码的小产品 

以下是产品使用的动态图

效果图显示

 

全部代码使用原生js写的, 样式使用了bootstrap,对原有页面有稍微一些影响.但不妨碍浏览信息. 下载报表使用的是html2canvas

高级功能 个人榜单  图表显示 需要使用星巴克咖啡充值 解锁 

 

做这个功能的时候,感觉界面的味道不够,于是找了一张背包行者的图片, 瞬间界面提升了几个逼格...哈哈哈...于是我找到了制作一个优秀产品的小技巧, 善用背景  背景音乐, 背景图片, 都是很好的技巧. 

关键代码如下: 

欢迎点在,留言讨论. 关注我的TM专栏 有更多硬核干货

// ==UserScript==
// @name         RM
// @namespace    https://fizzz.blog.csdn.net/
// @version      0.1
// @description  Report Maker: 基于页面结构化数据生成报表
// @author       Fizz
// @match        https://**/**
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @require      https://cdn.staticfile.org/html2canvas/0.5.0-beta4/html2canvas.min.js
// @resource bootstrapcss https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css

// ==/UserScript==

(function() {
  'use strict';
  let pageTitle = ''
  let groupTotalPerson = {'第一组':37,'第二组':37,'第三组': 39} // 各组总人数

  // 获取页面表格数据
  function getPageTableData () {
    let pageTableData = []
    let allTr = document.querySelectorAll('#main-content .confluenceTable tbody [role="row"]'); // 获取所有行
    [...allTr].forEach(trItem => {
        let allTrInnerTd = trItem.querySelectorAll('td')
        let name = allTrInnerTd[0].innerHTML.trim()
        if (name !== '<br>' && name) {
          let groupName = allTrInnerTd[1].innerText
          let date = allTrInnerTd[2].innerText
          let distance = allTrInnerTd[3].innerText
          let integral = allTrInnerTd[4].innerText
          let img = allTrInnerTd[5].innerText
          let remark = allTrInnerTd[6].innerText
          let trDataItem = {name,date,distance,integral,img,remark, groupName}
          pageTableData.push(trDataItem)
        }
    })
    return pageTableData
  }

  // 添加样式
  function addStyle () {
    let bootstrapcss = GM_getResourceText('bootstrapcss')
    bootstrapcss = bootstrapcss + `
      .fizz_inject_wrap_div{position: fixed;font-size: 14px !important;width: 100%;display: flex;justify-content: center;height: 100%;z-index: 100;z-index: 999;top:0;background: #fff;padding-top:20px;background-image:url('https://cf.qizhidao.com/download/attachments/63810224/1hikedr.png?version=1&modificationDate=1576220765550&api=v2')}
      .text-danger.import-info{color: #CC9900;font-weight: 700;font-size:16px}
      .myinject{position: fixed;bottom: 20px;right: 20px;vertical-align: middle;text-align: center;cursor: pointer;z-index: 999;padding: 5px 14px;border-radius: 4px;background-color: #2e6da4;border-color: #2e6da4;opacity: .2;color: #fff;}
      .myinject:hover{opacity: 1;color: #fff;}
      .fizz_operation_div{position: fixed;right: 30px;bottom: 50px;}
      .fizz_operation_div * {margin: 0;padding: 0;border: 0;outline: 0;font-size: 14px;box-sizing: content-box;}
      .fizz_operation_div ul,.fizz_operation_div li{list-style: none;border-left: 1px solid #2e6da4;border-right: 1px solid #2e6da4;margin: 0;padding: 0;outline: 0;font-size: 14px;box-sizing: content-box;}
      .fizz_operation_div li{padding: 6px 0px;text-align: center;cursor: pointer;vertical-align: middle;line-height: 20px;height: 20px;}
      .fizz_operation_div li:hover{background-color: #337ab7;color: #fff;}
      .fizz_operation_div .fizz_main_btn {border: none;outline: none;padding: 8px 14px;border-radius: 4px;background-color: #2e6da4;border-color: #2e6da4;opacity: .5;color: #fff;vertical-align: middle;text-align: center;cursor: pointer;line-height: 16px;vertical-align: middle;}
      .fizz_operation_div .fizz_main_btn:hover{opacity: 1;}
      .fizz_operation_div .fizz_menu_ul{display: none;}
      .fizz_operation_div:hover .fizz_menu_ul{display: block;}
      .hidden{display:none}
    `
    GM_addStyle(bootstrapcss)
  }

  // 处理表格数据,返回Map
  function showRes (pageTableData) {
    const resMap = new Map();
    // ...
    let groupHtmlStringObj = ceateResTableDiv(resMap)
    dataRender(groupHtmlStringObj)
  }

  // 创建显示结果的Div resMap:{'A': {},'B': {}}
  function ceateResTableDiv (resMap) {
    let groupHtmlStringObj = {
      '第一组': {groupTotalIntegral: 0, groupTotalDistance: 0, groupTotalJoinNum: 0, finalTotalIntegral: 0, joinPersonData: []},
      '第二组': {groupTotalIntegral: 0, groupTotalDistance: 0, groupTotalJoinNum: 0, finalTotalIntegral: 0, joinPersonData: []},
      '第三组': {groupTotalIntegral: 0, groupTotalDistance: 0, groupTotalJoinNum: 0, finalTotalIntegral: 0, joinPersonData: []}
    }
    // ...
    return groupHtmlStringObj
  }

  // 表格数据渲染到页面 {'第一组':{groupTotalIntegral: 0, groupTotalDistance: 0, groupTotalJoinNum: 0, joinPersonData: []},'第二组': [], '第三组': []}
  function dataRender (groupObjData) {
    console.log(groupObjData)
    for(let [key,value] of Object.entries(groupObjData)) {
     // ...
    }
  }

  // 关闭报表页面,视图
  function closeReportView () {
    document.querySelector('.fizz_inject_wrap_div').classList.add('hidden')
  }

  // 导出报表为图片
  function exportReportImg () {
    // ...
  }

  // 创建报表页面
  function creatReportWrapDiv () {
    let reportWrapDiv = document.createElement('div')
    let htmlString = `
      <div>
        ....
      </div>
    `
    reportWrapDiv.innerHTML = htmlString
    reportWrapDiv.classList.add('fizz_inject_wrap_div')
    reportWrapDiv.classList.add('hidden')
    return reportWrapDiv
  }

  // 显示报表图标视图
  function showReportChatView () {
    alert('此功能仅限高级会员使用,请使用星巴克咖啡充值解锁')
  }

  // 显示个人维度图标视图
  function showPersonView () {
    alert('此功能仅限高级会员使用,请使用星巴克咖啡充值解锁')
  }

  // 页面初始化函数
  function initFun () {
    pageTitle = document.querySelector('#title-text').innerText
    let body = document.querySelector('body')
    let injectDiv = document.createElement('div')
    injectDiv.classList.add('myinject')
    injectDiv.title = '生成报表'
    injectDiv.innerHTML = `R  M`
    injectDiv.onclick = function (e) {
      document.querySelector('.fizz_inject_wrap_div').classList.remove('hidden')
      let pageTableData = getPageTableData()
      showRes(pageTableData)
    }
    addStyle()
    body.appendChild(injectDiv)
    let reportWrapDiv = creatReportWrapDiv()
    body.appendChild(reportWrapDiv)

    document.querySelector('#fizz_rm_close_report').addEventListener('click', closeReportView, false)
    document.querySelector('#fizz_rm_export_report_img').addEventListener('click', exportReportImg, false)
    document.querySelector('#fizz_rm_report_chart_img').addEventListener('click', showReportChatView, false)
    document.querySelector('#fizz_rm_person_rank_img').addEventListener('click', showPersonView, false)
  }

  initFun()

})();

 

最后:

希望更多的人能够使用软件提高自己的工作效率,多点时间陪家人.多点时间去做自己喜欢做的事情.

 

VB控件 RMReport7 转:RMReport的使用方法及详解 1、不打印特定的MemoView,套打常用 a.页面设置-->其它-->不打印背景图 b.设置MemoView属性printable=False 2、 如何打印wwDBGrid? 修改rm.inc,如果想支持RX,GIF,JPEG,DimandAccess,Halcyon,DBISAM, EHLib,也需要修改rm.inc //{$DEFINE InfoPower} //修改这行,去掉"//" //{$Ehlib} 3.试用版安装方法(以下假设将文件释放到c:/rm目录中) (1)Tools->Environments Option->Libary->Libary Path中增加: c:/rm/souce c:/rm/bpl $(DELPHI)/Lib $(DELPHI)/Bin $(DELPHI)/Imports $(DELPHI)/Projects/Bpl (2)Component->Install Packages->Add,选bpl/rm_d70.bpl 4.在Delphi IDE中卸载以前的Report Machine版本,然后打开rm_r50.dpk,选"compile", 在打开rm_d50.dpk,选"Install". 包分成了Runtime package和Designer package,所以要安装顺序安装 5、单元格的变量格式用代码设置 t = TRMGridReportPage(RMGridReport1.Pages[0]).Grid.Cells[1, 1].View t = TRMMemoView(RMReport1.FindObject('memo1')); t.DisplayFormat := 'N0.001' //数字型 t.DisplayFormat := 'Dyyyy/mm/dd' //日期型 6、两遍报表如何用代码设置 GridReport1.DoublePass := True 7、用代码写数据字典: RMReport1.Dictionary.FieldAliases.Clear; RMReport1.Dictionary.FieldAliases['RMDBDataSet1'] := '动物'; RMReport1.Dictionary.FieldAliases['RMDBDataSet1."Name"'] := '姓名'; 这样在RM的设计器显示为自定义名称,为最终用户提供友好的显示 8、在报表中如何使用变量(或者如何给某个memoview赋值) a.RMVariables在RM_Class.pas中定义,是全局变量,这样定义后就可以在报表中使用变量"var1",例如: RMVariables['变量名称'] := Edit1.Text; b.用报表中数据字典,TRMReport.Dictionary.Variables,需要注意的是,如果变量是字符型的需要用AsString赋值,其他类型的用RMReport.Dictionary.Variables['var1'] := 1234,例如: RMReport1.LoadFromFile('1.rls'); RMReport1.Dictionary.Variables.AsString['变量名称'] := Edit1.Text; c. 直接对某个单元格赋值,例如: RMGridReport1.LoadFromFile('1.rls'); TRMGridReportPage(RMGridReport1.Pages[0]).Grid.Cells[1,1].Text := '值'; 如果是RMReportRMReport1.LoadFromFile('1.rmf'); t := RMReport1.FindObject('Memo1'); if t nil then // var t: TRMView t.Memo.Text := 'dsdsdsds'; d.脚本中直接引用Form的值 procedure Main; begin Memo1.Memo.Text := Form1.Edit1.Text; end; 9、自动换行 主项数据栏Stretched = true 文本框 Stretched = true WordWrap = true 10、RM内置变量(Script),增加中.... a.属性PrintAtAppendBlank=True CurReport.AppendBlanking=True时代表增加空行 在RM中,打印设置只能保存页面边距及打印份数、是否两遍打印以及
版本:1.0.0.1 更新日志: 1. 修正部分用户使用中用到插件的分割符号,致使插件不能正常使用,本版中间使用xml格式,不会和用户使用的符号有冲突。 2. 重新构件了ocx,jar两个地方的源码,使结构更 3. 增加图片图表的支持 4。 增加了在线设计 5。增加对每个按纽是否可用的控制 注意事项: 1. 这个版本的功能和前一个版本的功能是一样的,如果运行得可以就可以不用更新 严重声名: 插件用到的第三方控件如果涉及版权问题,请自觉向其所再单位获得授权。 声明: 任何个人或组织可以自由使用,可以任意修改、复制、发布,但要保留本人的声明以遵重本人的劳动成果,并且不能用于任何商业目的。 本人提供的是免费报表,不与商家争长短,但希望对你有用: 使用方法: 范例使用的是access数据库,首先在odbc 注册ReportDemo.mdb 数据源,名字随你喜好, 然后修改classes文件下的 db.properties 的连接。 然后把项目放在tomcat下运行既可: 起作用的关键文件:RMVIEWP.CAB 文件,客户端插件 Lib下的两个包:rmview1.0.0.1.jar 使用接口: package common.rmview; /* * 设计的第一步,首先定义字段的类型, * 然后通过AddRow将结果加入缓存中 * */ import common.rmview.surperInterface.IDataSet; import common.rmview.until.Types; public interface IRMDataSet extends IDataSet { public void addField(String fieldName, Types.filedType type, int fieldLength, Boolean isNull);//1:将段类型加入 public void addField(String fieldName, Types type, int fieldLength);//1:将段类型加入,isNull 默认为 false public void AddRow(IRMDataRecord rmd);//2:在 IRMDataRecord实现类赋值完成一条记录后使用一次,循环加入. } package common.rmview; import common.rmview.surperInterface.IDataRecord; /* * 使用时的第二步 * 通过这个接口设置结果集 */ public interface IRMDataRecord extends IDataRecord { public void SetValue(String fieldName,Object objValue); } package common.rmview; /* * 第三步: * 前面IDataSet,IRMDataRecord实现类完成后,通过此接口的实现类向html也面输出ie到ie以方便ocx的取值使用 */ public interface IRMEngine { final int rmf=1; final int rls=2; public void SetReportFile(String urlandFileName,int fileFormat);//1:设置报表路径及文件名称 public void AddClientDataSet(String dataSourceName,IRMDataSet rmd);//2:设置数据源名称以ocx使用,并把IRMDataSet实现的结果加入 public String CreateViewer();//3:生成在html页面的编码 } 具体的使用方法请参照 index.jsp 报表的使用方法请自己参照官方的文档 RM报表的设计请另行学习。 需要更高版本 请联系作者购买 新建QQ群:29672297 有疑问请发邮件到 lujianfu123@yahoo.com.cn 下期计划:进行对 fastReport的封装。不过也要看呼声如何再决定。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拿我格子衫来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值