SmartPP Helper

增强分发系统功能,检验分发系统异常值

// ==UserScript==
// @name         SmartPP Helper
// @namespace    http://minhill.com
// @version      0.4.3
// @description  增强分发系统功能,检验分发系统异常值
// @author       Minhill
// @include      http://10.148.16.64:8080/*
// @include      http://10.148.16.63:8080/*
// @include      http://10.148.16.40:8080/*
// @require      https://lib.baomitu.com/dayjs/1.10.4/dayjs.min.js
// @require      https://lib.baomitu.com/dayjs/1.10.4/plugin/customParseFormat.min.js
// @grant        GM_addStyle
// @supportURL  https://greasyfork.org/scripts/415457
// @homepage    https://greasyfork.org/zh-CN/scripts/415457
// @note        2021/03/11 增加中期表格转换功能
// ==/UserScript==

(function () {
  'use strict';
  let config = {
    keyword: {
      CN: {
        tMax: '最高温度',
        tMin: '最低温度',
        fcTime: '预报时效',
        timeSplit: '至',
        hour08: '08时',
        hourReg: /(\d{2})时/,
      },
      EN: {
        tMax: 'Maximum Temperature',
        tMin: 'Minimum Temperature',
        fcTime: 'Forecast Period',
        timeSplit: ' - ',
        hour08: '08:00',
        hourReg: /(\d{2}):00/,
      }
    },
    dayList:['周日','周一','周二','周三','周四','周五','周六',],
  }

  /**
   * 工具模块
   */
  const utils = {
    /**
     * 创建表格
     */
    createTable(tableParam = {
      header: ['最高温', '周一', '周二'], rows: [
        [
          ['粤东'],
          ['1 - 1'],
          ['1 - 2'],
        ],
        [
          ['粤西'],
          ['2 - 1'],
          ['2 - 2']
        ],
      ], rowHeader: true,
    }) {
      const tableNode = document.createElement("table");
      if (tableParam.header) {
        let headNode = tableNode.createTHead();
        let newHeadRow = headNode.insertRow(-1);
        for (let iHeaderCell of tableParam.header) {
          let newTh = document.createElement('th');
          let newText = document.createTextNode(iHeaderCell);
          newTh.appendChild(newText);
          newHeadRow.appendChild(newTh);
        }
      }
      if (tableParam.rows) {
        for (let iRow of tableParam.rows) {
          let newRow = tableNode.insertRow(-1);
          if (iRow.length !== 0) {
            let cellList;
            if (tableParam.rowHeader) {
              let newTh = document.createElement('th');
              let newText = document.createTextNode(iRow[0]);
              newTh.appendChild(newText);
              newRow.appendChild(newTh);
              cellList = iRow.slice(1);// 去除第一个
            }else{
              cellList = iRow;
            }
            for (let iCell of cellList) {

              // 
              let newCell = newRow.insertCell(-1);
              let newText = document.createTextNode(iCell);
              newCell.appendChild(newText);
            }
          } else {
            throw new Error('未正确配置单元格');
          }
        }
      } else {
        throw new Error('未正确配置行数');
      }

      return tableNode;
    },
    toggle10dayTableRow(){
      const tMaxTable = document.querySelector('.my-tenday-table.t-max');
      if(tMaxTable){
        tMaxTable.classList.toggle('only-day6');
      }
      const tMinTable = document.querySelector('.my-tenday-table.t-min');
      if(tMinTable){
        tMinTable.classList.toggle('only-day6');
      }
    },
  }
  // var myTable = utils.createTable(tableParam);
  // document.body.append(myTable);

  /**
   * 
   * @param {Object} table 表 Html DOM
   */
  function validateTemp(table) {

    let IntlWords;
    if (table.textContent.includes('预报时效')) {
      // 中文
      IntlWords = config.keyword.CN;
    }
    else if (table.textContent.includes('Forecast')) {// 英文
      IntlWords = config.keyword.EN;
    } else {
      console.log('未识别语言');
      return;
    }

    var tBody = table.children[0];
    let TimeRow, TmaxRow, TminRow;
    for (let tr of tBody.children) {// 获取高低温行元素
      if (tr.textContent.includes(IntlWords.fcTime)) TimeRow = tr;// Forecast Period
      if (tr.textContent.includes(IntlWords.tMax)) TmaxRow = tr;// Maximum Temperature
      if (tr.textContent.includes(IntlWords.tMin)) TminRow = tr;// Minimum Temperature
    }
    if (!TmaxRow) return;// 没有最高最低温直接返回
    let [tMaxList, tMinList, timeList] = [[], [], []];
    for (let index = 1; index < TmaxRow.children.length; index++) {// 导入高低温数据
      tMinList.push(Number(TminRow.children[index].textContent));
      tMaxList.push(Number(TmaxRow.children[index].textContent));
      timeList.push(TimeRow.children[index].textContent);
    }

    let initHour = NaN, endHour = NaN;
    var timePeroidList = timeList[0].split(IntlWords.timeSplit);
    if (timePeroidList.length === 1) throw new Error('未识别的日期分隔符');
    let initHourMatched = timePeroidList[0].match(IntlWords.hourReg);
    if (initHourMatched) {
      initHour = Number(initHourMatched[1]);
    } else {
      throw new Error('未识别的日期格式' + timePeroidList[0]);
    }
    let endHourMatched = timePeroidList[1].match(IntlWords.hourReg);
    if (endHourMatched) {
      endHour = Number(endHourMatched[1]);
    } else {
      throw new Error('未识别的日期格式' + timePeroidList[1]);
    }

    let validTmaxList, validTminList;
    if ((initHour === 8 && endHour === 20) || (initHour === 20 && endHour === 8)) {
      validTmaxList = new Array(tMaxList.length / 2);
      validTminList = new Array(tMaxList.length / 2);
    } else {
      console.log(`非正点时次${initHour}, ${endHour}`);
      return;
    }
    for (let i = 0; i < validTmaxList.length; i++) {// 判断每一对温度
      if (initHour === 8 && endHour === 20) {
        validTmaxList[i] = tMaxList[i * 2] > tMaxList[i * 2 + 1];
        validTminList[i] = tMinList[i * 2] > tMinList[i * 2 + 1];
      } else if (initHour === 20 && endHour === 8) {
        validTmaxList[i] = tMaxList[i * 2] < tMaxList[i * 2 + 1];
        validTminList[i] = tMinList[i * 2] < tMinList[i * 2 + 1];
      } else {
        console.log(`非正点时次${initHour}, ${endHour}`);
        return;
      }
    }
    changeValidStatus(validTmaxList, TmaxRow);
    changeValidStatus(validTminList, TminRow);
  }

  /**
   * 
   * @param {Array} validList 判断列表
   * @param {Object} tempRow 温度行元素
   */
  function changeValidStatus(validList, tempRow) {
    validList.forEach((iValid, index) => {
      if (iValid) {
        tempRow.children[1 + index * 2].classList.add("valid-success");
        tempRow.children[1 + index * 2 + 1].classList.add("valid-success");
        tempRow.children[1 + index * 2].classList.remove("valid-error");
        tempRow.children[1 + index * 2 + 1].classList.remove("valid-error");
      } else {
        tempRow.children[1 + index * 2].classList.add("valid-error");
        tempRow.children[1 + index * 2 + 1].classList.add("valid-error");
        tempRow.children[1 + index * 2].classList.remove("valid-success");
        tempRow.children[1 + index * 2 + 1].classList.remove("valid-success");
      }
    });
  }

  /**
  * 处理10天预报
  */
  function handle10daysForecast(table) {
    if(!table) table = document.querySelector('table.text_table3');

    var tBody = table.children[0];
// 获取表头
    var timeNode = document.querySelector('.forecast_time');
    var timeMatch = timeNode.textContent.match(/[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30)))/);
    var timeString;
    if(timeMatch){
      timeString = timeMatch[0];
    }
    var headerNodes = tBody.querySelectorAll('tr:first-child th[colspan]');
    var headerTimeList = [];
    for(let i = 0; i<headerNodes.length;i++){
      let iNode = headerNodes[i];
      let iText = iNode.textContent;
      let splitStrArr = iText.split('至');
      splitStrArr = splitStrArr.map(text=>text.trim());
      // let whichYear = dayjs().format('YYYY');
      // let whichMonth = dayjs().format('MM');
      // let whichDate = splitStrArr[1].slice(0, 2);
      // console.log(`${whichYear}-${whichMonth}-${whichDate}T20:00:00+08:00`);
      let whichDay;
      if(timeString){
        whichDay = dayjs(timeString).add(i+1,'day').day();
      }else{
        whichDay = dayjs().add(i,'day').day();
      }
      
      let timeObj = {
        text: iText,
        timeList: splitStrArr,
        day: config.dayList[whichDay],
      }
      headerTimeList.push(timeObj);
    }
    // console.log(headerTimeList);

    var allCols = tBody.querySelectorAll('tr.text_TR1');
    // console.log(allCols);
    var tableList = [];

    for (let iCol of allCols) {
      let city = iCol.children[0].textContent;
      let dataList = [];
      for (let i = 0; i < 10; i++) {
        let Tmax = Number(iCol.children[i * 3 + 2].textContent);
        let Tmin = Number(iCol.children[i * 3 + 3].textContent);
        dataList.push({
          Tmax,
          Tmin
        });
      }
      tableList.push({
        city,
        dataList
      });
    }
    // console.log(tableList);
    var regionList = [{
      name: '粤东',
      cityList: ['潮州', '饶平', '汕头', '潮阳', '澄海', '南澳', '汕尾', '海丰', '陆丰', '揭阳', '普宁', '揭西', '惠来',],
    },
    {
      name: '粤西',
      cityList: ['云浮', '郁南', '罗定', '新兴', '阳江', '阳春', '茂名', '电白', '信宜', '化州', '高州', '湛江', '遂溪', '廉江', '吴川', '雷州', '徐闻',],
    },
    {
      name: '粤北',
      cityList: ['韶关', '新丰', '翁源', '始兴', '乳源', '南雄', '仁化', '乐昌', '清远', '连南', '连州', '连山', '阳山', '佛冈', '英德', '河源', '连平', '和平', '龙川', '紫金', '梅县', '丰顺', '平远', '兴宁', '蕉岭', '大埔', '五华',],
    },
    {
      name: '珠三角',
      cityList: ['广州', '花都', '从化', '番禺', '增城', '南海', '顺德', '三水', '高要', '封开', '德庆', '怀集', '广宁', '四会', '东莞', '深圳', '惠阳', '龙门', '博罗', '惠东', '珠海', '斗门', '中山', '新会', '上川', '鹤山', '开平', '恩平', '台山'],
    },
    ];

    for (let iRegion of regionList) {// 归类
      iRegion.dataList = iRegion.cityList.map(iCity => {
        let foundCity = tableList.find(iCol => {
          //       console.log(iCol.city);
          return iCol.city.indexOf(iCity.trim()) !== -1
        });
        // console.log(foundCity);
        if (!foundCity) {
          alert('未找到需要的市县: ' + iCity);
          throw new Error('未找到需要的市县: ' + iCity);
        } else {
          return foundCity;
        }
      })
    };

    var fcList10 = regionList.map(iRegion => {
      const cityCount = iRegion.cityList.length;
      let TmaxList = new Array(10).fill(0);
      let TminList = new Array(10).fill(0);
      for (let iCity = 0; iCity < cityCount; iCity++) {// 求和
        let iTmaxList = iRegion.dataList[iCity].dataList.map(v => v.Tmax);
        TmaxList = TmaxList.map((v, i) => v + iTmaxList[i]);

        let iTminList = iRegion.dataList[iCity].dataList.map(v => v.Tmin);
        TminList = TminList.map((v, i) => v + iTminList[i]);
      }
      TmaxList = TmaxList.map(v => v / cityCount);
      TminList = TminList.map(v => v / cityCount);
      return {
        name: iRegion.name,
        TmaxList,
        TminList,
      }
    });
    fcList10.time = headerTimeList;
    console.log(fcList10);
    var tableTmaxData = {
      header:['最高气温'].concat(headerTimeList.map(iTime=>iTime.day)),
      rows: fcList10.map(region=>{
        let TmaxStrList = region.TmaxList.map(v=>v.toFixed(1));
        return [region.name,].concat(TmaxStrList);
      }),
      rowHeader: true,
    }
    // 判断overlay 是否生成
    let overlayCreated = document.querySelector('.my-overlay');
    let overlay;
    if(overlayCreated){
      overlay = overlayCreated;
      overlay.innerHTML = '';
    }else{
      overlay = document.createElement('div');
      overlay.setAttribute('class', 'my-overlay hidden');
      document.body.append(overlay);
    }
    // 表格切换按钮
    var hiddenRowBtn = document.createElement('button');
    hiddenRowBtn.textContent = '切换表格显示(10天/后6天)';
    hiddenRowBtn.addEventListener('click', utils.toggle10dayTableRow);
    overlay.appendChild(hiddenRowBtn);
    // 最高气温
    var tableTmax = utils.createTable(tableTmaxData);
    // overlay.setAttribute('class', 'my-overlay hidden');
    tableTmax.setAttribute('class', 'my-tenday-table t-max');
    overlay.appendChild(tableTmax);
    // document.body.append(overlay);

    // 最低气温
    var tableTminData = {
      header:['最低气温'].concat(headerTimeList.map(iTime=>iTime.day)),
      rows: fcList10.map(region=>{
        let TminStrList = region.TminList.map(v=>v.toFixed(1));
        return [region.name,].concat(TminStrList);
      }),
      rowHeader: true,
    }
    var tableTmin = utils.createTable(tableTminData);
    tableTmin.setAttribute('class', 'my-tenday-table t-min');
    overlay.appendChild(tableTmin);
  }

  GM_addStyle(
    `
    .hidden{
      display:none;
    }
    .my-btn-overlay{
      z-index: 10;
      position: fixed;
      right: 120px;
      top: 185px;
    }

    .my-overlay{
      z-index: 10;
      position: fixed;
      left: 50%;
      right: 50%;
      top: 0px;
      left: 0px;
      right: 0px;
      margin-left: auto;
      margin-right: auto;
      width: 800px;
      background: white;
    }
    .my-tenday-table {
      margin-top:20px;
      border-collapse: collapse;
      border-spacing: 0;
      empty-cells: show;
      border: 1px solid #cbcbcb;
  }
   
  .my-tenday-table caption {
      color: #000;
      font: italic 85%/1 arial,sans-serif;
      padding: 1em 0;
      text-align: center;
  }
   
  .my-tenday-table td,.my-tenday-table th {
      border: 1px solid #cbcbcb;
      border-width: 1px;
      font-size: inherit;
      margin: 0;
      overflow: visible;
      padding: .5em 1em;
  }
   
  .my-tenday-table thead {
      color: #000;
      text-align: left;
      vertical-align: bottom;
  }
   
  .my-tenday-table td {
      background-color: transparent;
  }
    
  .my-tenday-table.only-day6 td:nth-child(2),
  .my-tenday-table.only-day6 td:nth-child(3),
  .my-tenday-table.only-day6 td:nth-child(4),
  .my-tenday-table.only-day6 td:nth-child(5){
    display: none;
  }
  .my-tenday-table.only-day6 tr:first-child th:nth-child(2),
  .my-tenday-table.only-day6 tr:first-child th:nth-child(3),
  .my-tenday-table.only-day6 tr:first-child th:nth-child(4),
  .my-tenday-table.only-day6 tr:first-child th:nth-child(5){ 
    display: none;
  }
    `
  )

  /**
   * 获取表格
   */
  function tableSelector() {
    var tableList = document.querySelectorAll('table.text_table2');
    for (let iTable of tableList) {
      validateTemp(iTable);
    }
    // console.log('页面变动监测');
    let myBtnOverlay = document.querySelector('.my-btn-overlay');
    
    let fcTitle = document.querySelector('.forecast_title');
    // console.log(fcTitle);
    if (fcTitle && fcTitle.textContent.indexOf('10天天气预报') !== -1) {
      // console.log('10天预报');
      let table3 = document.querySelector('table.text_table3');
      if (table3){
        handle10daysForecast(table3);
        if(myBtnOverlay){
          // 有按钮显示
          myBtnOverlay.classList.remove('hidden');
        }else{
          createFloatBtn();// 没有按钮创建
        }
      }
    }else{
      if(myBtnOverlay){
        myBtnOverlay.classList.add('hidden');
      }
      let myOverlay = document.querySelector('.my-overlay');
      myOverlay && myOverlay.classList.add('hidden');
    }
  }

  /**
   * 创建浮动按钮
   */
  function createFloatBtn() {
    
    let btnOverlay = document.createElement('div');
    btnOverlay.setAttribute('class', 'my-btn-overlay');
    let tenDayBtn = document.createElement('button');
    
    tenDayBtn.addEventListener('click', ()=>{
      let tenDayOverlay = document.querySelector('.my-overlay');
      if(tenDayOverlay){
        tenDayOverlay.classList.toggle('hidden');
      }
    });
    tenDayBtn.textContent = '分片统计表';
    btnOverlay.appendChild(tenDayBtn);
    document.body.append(btnOverlay);
  }
  /**
   * 模板部分监听器
   */
  function templateObserver() {
    const targetNode = document.getElementById('templatePreContent');// 选择需要观察变动的节点
    const config = { childList: true, subtree: false };// 观察器的配置(需要观察什么变动)
    const callback = function (mutationsList, observer) {// 当观察到变动时执行的回调函数
      tableSelector();
    };
    const observer = new MutationObserver(callback);// 创建一个观察器实例并传入回调函数
    observer.observe(targetNode, config);// 以上述配置开始观察目标节点
    return tableSelector();
  }

  /**
   * 主入口函数
   */
  function main() {
    var style = '<style>.valid-success{background-color:lightgreen;}.valid-error{background-color:orange;}</style>';
    var ele = document.createElement('div');
    ele.innerHTML = style;
    document.getElementsByTagName('head')[0].appendChild(ele.firstElementChild);
    lookUpTarget();
  }

  /**
   * 处理注入问题
   */
  function lookUpTarget() {
    const targetNode = document.getElementById('templatePreContent');
    if (targetNode) {
      return templateObserver()
    } else {
      console.log('未找到目标元素,等待5秒重新查找');
      return setTimeout(lookUpTarget, 5000);
    }
  }

  main();

})();