Greasy Fork镜像 支持简体中文。

禅道任务统计(自用)

统计禅道任务的工时

  1. // ==UserScript==
  2. // @name 禅道任务统计(自用)
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.50
  5. // @description 统计禅道任务的工时
  6. // @author zyb
  7. // @match http://zentao.ngarihealth.com/index.php?*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=ngarihealth.com
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. class NewNavBar {
  17.  
  18. navDom = document.querySelectorAll('#mainHeader #navbar ul')[0];
  19. timeId = null;
  20. times = 0;
  21.  
  22. init() {
  23. this.timeId = setInterval(() => {
  24. if (this.navDom) {
  25. this.setNewNavBar();
  26. clearInterval(this.timeId);
  27. } else {
  28. this.navDom = document.querySelectorAll('#mainHeader #navbar ul')[0];
  29. this.times++;
  30. }
  31.  
  32. if (this.times > 20) {
  33. console.log('------------------');
  34. console.log('未取到关键dom节点【#navbar ul】,请重试!');
  35. console.log('------------------');
  36. clearInterval(this.timeId);
  37. }
  38. }, 100)
  39.  
  40. }
  41.  
  42. setNewNavBar(){
  43. let liDomArray = Array.from(this.navDom.querySelectorAll('li'));
  44. let liDividerDom = this.createDom({
  45. element: 'li',
  46. attributeArr: [{ class: 'divider' }],
  47. innerHTML:''
  48. });
  49. let taskLiDom = this.createDom({
  50. element: 'li',
  51. attributeArr: [{ 'data-id': 'task' }],
  52. innerHTML:'<a href="/index.php?m=my&amp;f=task">任务</a>'
  53. });
  54. let bugLiDom = this.createDom({
  55. element: 'li',
  56. attributeArr: [{ 'data-id': 'bug' }],
  57. innerHTML:'<a href="/index.php?m=my&amp;f=bug">Bug</a>'
  58. });
  59. liDomArray.unshift(liDividerDom);
  60. liDomArray.unshift(bugLiDom);
  61. liDomArray.unshift(taskLiDom);
  62.  
  63. this.navDom.innerHTML = '';
  64.  
  65. liDomArray.forEach((item) => {
  66. this.navDom.appendChild(item)
  67. })
  68. }
  69.  
  70. createDom(obj){
  71. let {element,attributeArr,innerHTML} = obj;
  72.  
  73. if(!element){
  74. return;
  75. }
  76.  
  77. let dom = document.createElement(element);
  78.  
  79. attributeArr && attributeArr.length && attributeArr.forEach(function(obj){
  80. let arr = Object.entries(obj)[0];
  81. dom.setAttribute(arr[0], arr[1]);
  82. })
  83.  
  84. innerHTML && (dom.innerHTML = innerHTML);
  85.  
  86. return dom;
  87. }
  88.  
  89. }
  90.  
  91. class StatisticsWorkHours {
  92. // 统计工时的总节点
  93. statisticsWorkHourDivDom = null;
  94. // 统计工时按钮的Dom节点
  95. statisticsWorkHourBtnDivDom = null;
  96. // 显示工时的Dom节点
  97. displayWorkHourDivDom = null;
  98.  
  99. timeId = null;
  100. times = 0;
  101. clickFlag = true;
  102. mainMenuDom = document.querySelectorAll('#mainMenu')[0];
  103.  
  104. init() {
  105.  
  106. this.timeId = setInterval(() => {
  107. if (this.mainMenuDom) {
  108. this.creatDom();
  109. clearInterval(this.timeId);
  110. } else {
  111. this.mainMenuDom = document.querySelectorAll('#mainMenu')[0];
  112. this.times++;
  113. }
  114.  
  115. if (this.times > 20) {
  116. console.log('------------------');
  117. console.log('未取到关键dom节点【#mainMenu】,请重试!');
  118. console.log('------------------');
  119. clearInterval(this.timeId);
  120. }
  121. }, 100)
  122. }
  123.  
  124. // 初始化函数
  125. creatDom() {
  126. let _this = this;
  127.  
  128. if (!this.mainMenuDom) {
  129. alert('未取到关键dom节点【#mainMenu】,请重试!');
  130. return;
  131. }
  132.  
  133. // 创建css样式
  134. const head = document.head;
  135. const style = document.createElement('style');
  136. const cssStr = `
  137. #statisticsWorkHourDivDom {
  138. height:33px;
  139. display:flex;
  140. align-items: center;
  141. padding:10px;
  142. }
  143.  
  144. #statisticsWorkHourBtnDivDom{
  145. background-color:#0c64eb;
  146. color:#fff;
  147. border-radius:5px;
  148. padding:5px;
  149. cursor: pointer;
  150. }
  151.  
  152. #displayWorkHourDivDom{
  153. display:flex;
  154. align-items: center;
  155. padding:0 10px;
  156. }
  157.  
  158. #displayWorkHourDivDom p{
  159. margin:0;
  160. }
  161.  
  162. #displayWorkHourDivDom span{
  163. color:red;
  164. }
  165. `;
  166. const style_text = document.createTextNode(cssStr);
  167. style.appendChild(style_text);
  168. head.appendChild(style);
  169.  
  170. // 创建统计工时的总节点
  171. this.statisticsWorkHourDivDom = document.createElement('div');
  172. this.statisticsWorkHourDivDom.setAttribute('id', 'statisticsWorkHourDivDom');
  173. // 统计工时按钮的Dom节点
  174. this.statisticsWorkHourBtnDivDom = document.createElement('div');
  175. this.statisticsWorkHourBtnDivDom.setAttribute('id', 'statisticsWorkHourBtnDivDom');
  176. this.statisticsWorkHourBtnDivDom.innerHTML = '统计工时';
  177. // 显示工时的Dom节点
  178. this.displayWorkHourDivDom = document.createElement('div');
  179. this.displayWorkHourDivDom.setAttribute('id', 'displayWorkHourDivDom');
  180.  
  181. // 将各个dom节点填充到页面中
  182. this.mainMenuDom.appendChild(this.statisticsWorkHourDivDom);
  183. //添加按钮到dom节点
  184. this.statisticsWorkHourDivDom.appendChild(this.statisticsWorkHourBtnDivDom);
  185. this.statisticsWorkHourDivDom.appendChild(this.displayWorkHourDivDom);
  186.  
  187. // 点击按钮触发
  188. this.statisticsWorkHourBtnDivDom.onclick = function () {
  189. if (!_this.clickFlag) {
  190. console.log("重复点击");
  191. return;
  192. }
  193. _this.displayWorkHourDivDom.innerHTML = '统计中...';
  194. _this.clickFlag = false;
  195. // 统计禅道任务的工时
  196. _this.getWorkHour();
  197. }
  198.  
  199. }
  200.  
  201. // 统计禅道任务的工时
  202. getWorkHour() {
  203.  
  204. // 数据处理,以便于后续操作
  205. // 获取需求列表的dom节点,请将每页选项调整到40或更多
  206. let trListDom = Array.from(document.querySelectorAll('#main #mainContent tbody tr')) || [];
  207. // 任务需求数组
  208. let requirementsList = trListDom.map(itemDom => {
  209. // id
  210. const id = itemDom.querySelectorAll('.c-id')[0].querySelectorAll('.checkbox-primary input')[0].value;
  211. // 预计开始日期
  212. const dateStr = itemDom.querySelectorAll('td:nth-child(6)')[0].innerText || '';
  213. // 预计工时
  214. const estimateWorkHour = +itemDom.querySelectorAll('td:nth-child(10)')[0].innerText || 0;
  215. // 消耗工时
  216. const consumeWorkHour = +itemDom.querySelectorAll('td:nth-child(11)')[0].innerText || 0;
  217.  
  218. return { dateStr, estimateWorkHour, consumeWorkHour, id }
  219. }) || [];
  220.  
  221. // 处理异步请求,兼容不需要请求接口的数据
  222. const promiseList = requirementsList.map(async (item) => {
  223. let promise;
  224. if (!item.dateStr) {
  225. // item.dateStr为空,表示是被分配的需求
  226.  
  227. // 设置请求的url
  228. let url = `http://zentao.ngarihealth.com/index.php?m=task&f=view&taskID=${item.id}`
  229. // 将异步请求转为同步,目的是减少服务器压力,以免被检测后封锁ip
  230. promise = await this.ajaxAsyncFuc({ url });
  231.  
  232. } else {
  233. // item.dateStr不为空,表示是自己创建的需求,不需要额外调用接口获取日期
  234.  
  235. // 新建Promise对象,返回值为空
  236. promise = await new Promise((resolve, reject) => {
  237. resolve();
  238. })
  239. }
  240.  
  241. return { data: promise, object: item };
  242.  
  243. })
  244.  
  245. // 等待所有请求完成后,对不同月份的数据进行处理
  246. Promise.all(promiseList).then(res => {
  247. // 是否统计被分配的需求的工时标识
  248. let tipsFlag = true;
  249.  
  250. // 处理完后存储的数据
  251. let dataForMonthObj = {
  252. // 本月的月份
  253. nowMonthStr: '',
  254. // 本月预计工时
  255. estimateWorkHour: 0,
  256. // 本月实际消耗工时
  257. consumeWorkHour: 0,
  258. // 上个月的月份
  259. preMonthStr: '',
  260. // 上个月预计工时
  261. preEstimateWorkHour: 0,
  262. // 上个月实际消耗工时
  263. preConsumeWorkHour: 0,
  264. // 这周消耗的工时
  265. thisWeekConsumeWorkHour: 0,
  266. // 上周消耗的工时
  267. lastWeekConsumeWorkHour: 0,
  268. };
  269.  
  270. // 遍历返回值
  271. res.map(item => {
  272. let obj = {};
  273.  
  274. if (item.data) {
  275. // data字段有数据,说明是被分配的需求
  276.  
  277. // 创建一个div的Dom节点,将接口返回的HTML字符串转为Dom节点
  278. let div = document.createElement('div');
  279. div.innerHTML = item.data;
  280. const trDom = div.querySelectorAll('#legendLife tbody tr:nth-child(2)')[0];
  281. const value = trDom.querySelectorAll('td')[0].innerText || '';
  282. // 创建正则规则,匹配yyyy-MM-dd或yyyy/MM/dd时间格式
  283. const regex = /\d{4}[-/]\d{2}[-/]\d{2}/g;
  284.  
  285. obj = {
  286. ...item.object,
  287. // 截取符合正则规则的字符串,即任务日期
  288. dateStr: value.match(regex) && value.match(regex)[0] || '',
  289. };
  290.  
  291. // data字段有数据,说明是被分配的需求,所以不需要提示文案
  292. tipsFlag = false;
  293.  
  294. } else {
  295. // data字段无数据,说明是自己创建的需求
  296.  
  297. obj = { ...item.object };
  298. }
  299.  
  300. // 日期处理
  301. // 本月的月份
  302. let nowMonth = new Date().getMonth() + 1;
  303. // 上个月的月份
  304. let previousMonth = (nowMonth === 1) ? 12 : (nowMonth - 1);
  305. // 本周周一的日期
  306. let mondayDate = new Date().getLastWeekday();
  307. // 上周一的日期
  308. let lastMondayDate = new Date(mondayDate.getTime() - 1000).getLastWeekday();
  309.  
  310.  
  311. // 如果dateStr为空,说明此任务还没完成
  312. if (obj.dateStr) {
  313.  
  314. // 当前数据的开始时间
  315. let date = new Date(obj.dateStr);
  316. // 当前数据的开始时间月份
  317. let month = date.getMonth() + 1;
  318.  
  319. // 如果是本月数据
  320. if (month === nowMonth) {
  321. dataForMonthObj.nowMonthStr = month;
  322. dataForMonthObj.estimateWorkHour += obj.estimateWorkHour;
  323. dataForMonthObj.consumeWorkHour += obj.consumeWorkHour;
  324. }
  325. // 如果是上月数据
  326. if (month === previousMonth) {
  327. dataForMonthObj.preMonthStr = month;
  328. dataForMonthObj.preEstimateWorkHour += obj.estimateWorkHour;
  329. dataForMonthObj.preConsumeWorkHour += obj.consumeWorkHour;
  330. }
  331.  
  332. // 如果是这周的数据
  333. if (date.getTime() > mondayDate.getTime()) {
  334. dataForMonthObj.thisWeekConsumeWorkHour += obj.consumeWorkHour;
  335. }
  336. // 如果是上周的数据
  337. if (date.getTime() < mondayDate.getTime() && date.getTime() > lastMondayDate.getTime()) {
  338. dataForMonthObj.lastWeekConsumeWorkHour += obj.consumeWorkHour;
  339. }
  340. }
  341.  
  342.  
  343. return obj
  344. })
  345. // 将处理完的数据显示到页面上
  346. this.setDivValueFuc(dataForMonthObj, tipsFlag);
  347. this.clickFlag = true;
  348. });
  349.  
  350. }
  351.  
  352. // 将处理完的数据显示到页面上
  353. setDivValueFuc(dataForMonthObj, tipsFlag) {
  354. this.displayWorkHourDivDom.innerHTML = `
  355. <p>${dataForMonthObj.nowMonthStr}月消耗时间:<span>${dataForMonthObj.consumeWorkHour}</span>工时;</p>
  356. <p>${dataForMonthObj.preMonthStr}月消耗时间:<span>${dataForMonthObj.preConsumeWorkHour}</span>工时;</p>
  357. <p>本周消耗时间:<span>${dataForMonthObj.thisWeekConsumeWorkHour}</span>工时;</p>
  358. <p>上周消耗时间:<span>${dataForMonthObj.lastWeekConsumeWorkHour}</span>工时;</p>
  359. <p>${tipsFlag ? ('<span>注意!未统计被分配的需求的工时</span>') : ('<span></span>')}</p>
  360. `;
  361. }
  362.  
  363. // 发送ajax数据
  364. async ajaxAsyncFuc(obj = {}) {
  365. return new Promise(function (resolve, reject) {
  366. // 发送数据
  367. const xhr = new XMLHttpRequest(); // 创建XMLHttpRequest对象
  368. const url = obj.url || ''; // 要访问的URL地址
  369. const contentType = obj.contentType || 'application/x-www-form-urlencoded'; // 设置请求头
  370. const type = obj.type || "GET"; // 定义请求方法
  371. const data = obj.data;
  372.  
  373. xhr.open(type, url); // 定义请求方法和URL
  374. xhr.setRequestHeader('Content-type', contentType); // 设置请求头
  375.  
  376. // 处理请求响应
  377. xhr.onreadystatechange = function () {
  378. if (xhr.readyState === 4) {
  379. if (xhr.status === 200) {
  380. // console.log(xhr.responseText); // 响应内容将会被打印到控制台
  381. resolve(xhr.responseText);
  382. } else {
  383. reject();
  384. }
  385. }
  386. }
  387.  
  388. // 发送POST请求
  389. xhr.send(data); // 动态添加请求数据
  390. })
  391. }
  392. }
  393.  
  394. // let taskHref = 'http://zentao.ngarihealth.com/index.php?m=my&f=task';
  395. let taskSearch = '?m=my&f=task';
  396. let { search } = location;
  397.  
  398. if (search.includes(taskSearch)) {
  399. // 统计工时
  400. let statisticsWorkHours = new StatisticsWorkHours();
  401. statisticsWorkHours.init();
  402. }
  403.  
  404. let newNavBar = new NewNavBar();
  405. newNavBar.init();
  406.  
  407. // <li class="divider"></li>
  408.  
  409. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址