奇葩的财务系统
事情是这样的,之前作为会计的老婆总给我吐槽他们公司的系统的奇葩设定。
公司财务方面级别分为 成本中心 - 办公室 - 项目
,它们分别有各自的编码和名称,一个成本中心下有多个办公室、一个办公室下有多个项目。
如果某个项目的员工想要费用报销,员工需要在每一条报销项上填上费用所属的项目和成本中心信息。
坑的点来了,由于是手填就会导致用户经常填错,出现项目、办公室、成本中心不一致的情况。需要财务肉!眼!核!对!
更坑的是,在系统单子中只显示项目名称和成本中心编码,所以财务人员需要用项目名称查所属办公室名称,再用成本中心编码查成本中心名称。最后比对办公室是否归属成本中心。
这系统的设计真的是让我惊呆了,不得不给设计者一个大大的赞。但没办法,受苦的是老婆。必须解决问题。是时候展现程序员对象的超能力了。
数据获取
财务系统的数据获取上,有一些是老婆公司提供的 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);
效果图
最后
感觉浏览器插件虽然简单,但可以做的事很多。就比如我常用的比价小工具、页面样式修改工具。后面如果对某个常用页面有哪里不爽了,可以直接写插件修改。 另外从安全角度来说,插件可以干的事儿还挺多的。所以不建议用一些冷门插件扩展。