用 Chrome 插件快速核对费用报销单

奇葩的财务系统

事情是这样的,之前作为会计的老婆总给我吐槽他们公司的系统的奇葩设定。

公司财务方面级别分为 成本中心 - 办公室 - 项目,它们分别有各自的编码和名称,一个成本中心下有多个办公室、一个办公室下有多个项目。

如果某个项目的员工想要费用报销,员工需要在每一条报销项上填上费用所属的项目和成本中心信息。

坑的点来了,由于是手填就会导致用户经常填错,出现项目、办公室、成本中心不一致的情况。需要财务肉!眼!核!对!

更坑的是,在系统单子中只显示项目名称和成本中心编码,所以财务人员需要用项目名称查所属办公室名称,再用成本中心编码查成本中心名称。最后比对办公室是否归属成本中心。

这系统的设计真的是让我惊呆了,不得不给设计者一个大大的赞。但没办法,受苦的是老婆。必须解决问题。是时候展现程序员对象的超能力了。

数据获取

财务系统的数据获取上,有一些是老婆公司提供的 excel 文档,通过 node 转成 JSON 使用,另外一些通过 puppeteer 模拟用户登录的过程,再模拟浏览器的 fetch() 来请求获取信息。

这方面后续可以再水一篇博客细说。

Chrome 插件开发

简单说下 Chrome 插件(chrome extension)的实现。

插件的创建与运行

开发 Chrome 插件, manifast.json 是唯一必要的文件。创建一个文件夹,在文件中创建一个 manifast.json 文件,然后写上一些基本信息就完成了一个简单插件。

json 复制代码
{
  "manifest_version": 3,
  "name": "小肥羊快速查询",
  "version": "1.0.0",
  "description": "一款通过脚本快速提醒的软件"
}

打开 chrome 的 扩展程序 - 管理扩展程序,打开开发者模式。然后点击"加载已解压的扩展程序"就可以运行我们的插件了。

Manifest 中一些重要的配置项

  • icons 设置插件的图标
  • permissions 管理插件用到的权限
  • background 运行在后台的 JS 脚本
  • content_scripts 插件运行时的 JS 脚本,可以设置不同触发时机。
  • action 浏览器右上角插件图标的交互
  • options_ui 插件配置界面,一般会打开一个 HTML 页面。
  • chrome_url_overrides 重写 chrome 打开新 tab 的行为,参考掘金插件。

逻辑实现

开搞!首先是完整的 Manifest.xml

json 复制代码
{
  "manifest_version": 3,
  "name": "小肥羊快速查询",
  "version": "1.0.0",
  "description": "一款通过脚本快速提醒的软件",

  "permissions": ["activeTab", "scripting"],
  "action": {
    "default_title": "我是小肥羊",
    "default_icon": "yang.png"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

由于功能简单,所以用到的配置项并不多。下面是 service-worker.js 的代码。功能设定是当用户点击插件图标后,开始注入 JS 脚本操作 DOM。

js 复制代码
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['/data.js', '/logic.js']
  });
});

所有的 JS 都是运行在 window 全局作用域下的,所以先将数据 data.js 放到作用域下,然后执行逻辑。

js 复制代码
const COST = [...]
const SOURCE = [...]
js 复制代码
function logic() {
  // 避免重复注入
  const sheepSpan = document.getElementById('mini-fat-sheep');
  if (sheepSpan) {
    sheepSpan.innerText = '肥羊虽好,但不要贪杯哦!';
    return;
  }

  const title = document.querySelector('#lblTitle');

  const table = document.querySelector('#Control71 > tbody');
  const rows = table ? table.getElementsByClassName('rows') : null;

  if (rows && rows.length > 0) {
    for (const row of rows) {
      const sourceDom = row.children[3].querySelector('input');
      const sourceName = sourceDom.value.replace('<br/>', ' ');
      const targetDom = row.children[9].querySelector('input');
      const targetCode = targetDom.value;

      const sourceInfo = SOURCE[sourceName];
      if (sourceInfo) {
        const wrapDOM = document.createElement('div');
        wrapDOM.style.color = '#CCC';
        wrapDOM.style.fontSize = '12px';

        sourceInfo.forEach((info) => {
          const infoDOM = document.createElement('div');
          infoDOM.style.borderTop = 'solid 1px #CCC';

          const projectCodeDOM = document.createElement('span');
          projectCodeDOM.innerText = '项目编码:' + info.projectCode;
          if (info.projectCode.startsWith('C')) {
            projectCodeDOM.style.color = '#e6a23c';
          }
          infoDOM.appendChild(projectCodeDOM);

          const officeCodeDOM = document.createElement('span');
          officeCodeDOM.innerText = '办公室名称:' + info.officeName;
          officeCodeDOM.style.margin = '0 5px';
          infoDOM.appendChild(officeCodeDOM);

          const checkDOM = document.createElement('span');
          if (
            getTargetName(targetCode).includes(info.officeName) &&
            info.officeName !== ''
          ) {
            checkDOM.innerText = '正确';
            checkDOM.style.color = '#67c23a';
          } else {
            checkDOM.innerText = '错误';
            checkDOM.style.color = '#f56c6c';
          }
          infoDOM.appendChild(checkDOM);

          wrapDOM.appendChild(infoDOM);
        });

        const parnetNode = sourceDom.parentNode;
        parnetNode.append(wrapDOM);
      }

      const costInfo = COST.filter((item) => item.CostOrgCode === targetCode);
      if (costInfo.length > 0) {
        const wrapDOM = document.createElement('div');
        wrapDOM.style.color = '#CCC';
        wrapDOM.style.fontSize = '12px';

        costInfo.forEach((info) => {
          const infoDOM = document.createElement('div');
          infoDOM.innerHTML = `${info.CostOrgName}`;
          infoDOM.style.borderTop = 'solid 1px #CCC';

          wrapDOM.appendChild(infoDOM);
        });

        const parnetNode = targetDom.parentNode;
        parnetNode.append(wrapDOM);
      }
    }
  }

  const header = document.querySelector('#divTopBars');
  const span = document.createElement('span');
  span.innerText = '放下武器,你已经被小肥羊接管';
  span.id = 'mini-fat-sheep';
  span.style.color = '#f56c6c';
  span.style.fontSize = '12px';
  span.style.position = 'absolute';
  span.style.bottom = '-20px';
  span.style.right = '10px';
  span.style.zIndex = '1000';
  header.appendChild(span);
}

function getTargetCode(name) {
  const findItem = COST.find((item) => item.CostOrgName.includes(name));
  if (findItem) {
    return findItem.CostOrgCode;
  }
  return '----';
}

function getTargetName(code) {
  const costMap = {};
  for (const cost of COST) {
    if (costMap[cost.CostOrgCode]) {
      costMap[cost.CostOrgCode] += '/' + cost.CostOrgName;
    } else {
      costMap[cost.CostOrgCode] = cost.CostOrgName;
    }
  }

  return costMap[code] || '';
}

logic();

其实,都是各种 DOM 操作。说下步骤~

  • 打开财务报表页面,通过开发者模式的 Element 面板来定位 DOM 元素。
  • 定位到了报销区域的表格,拿到表格中所有 row 元素进行遍历。table.getElementsByClassName('rows')
  • 取到表格中的特定值,这里取得是项目名称和成本中心编码。row.children[3].querySelector('input')
  • 用现有数据进行分析查找,再利用 DOM API 将需要注解的内容写到 DOM 中去。parnetNode.append(wrapDOM);

效果图

最后

感觉浏览器插件虽然简单,但可以做的事很多。就比如我常用的比价小工具、页面样式修改工具。后面如果对某个常用页面有哪里不爽了,可以直接写插件修改。 另外从安全角度来说,插件可以干的事儿还挺多的。所以不建议用一些冷门插件扩展。

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui