Greasy Fork is available in English.

Mortal 显示恶手率

Mortal牌谱解析增强脚本 (雀魂麻将/天凤(天鳳)(てんほう)/麻雀一番街)(Mahjong Soul/Tenhou/Riichi City)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Mortal 显示恶手率
// @name:zh      Mortal牌谱解析增强脚本
// @name:zh-CN   Mortal牌谱解析增强脚本
// @name:zh-TW   Mortal牌譜解析增強腳本
// @name:ja      Mortal牌譜レビュー強化スクリプト
// @name:ko      Mortal 게임 기록 감사 강화 스크립트
// @name:ru      Скрипт расширения аудита игровых записей Mortal
// @name:en      Mortal game record review enhancement script
// @description Mortal牌谱解析增强脚本 (雀魂麻将/天凤(天鳳)(てんほう)/麻雀一番街)(Mahjong Soul/Tenhou/Riichi City)
// @description:zh Mortal牌谱解析增强脚本 (雀魂麻将/天凤/麻雀一番街)
// @description:zh-CN Mortal牌谱解析增强脚本 (雀魂麻将/天凤/麻雀一番街)
// @description:zh-TW Mortal牌譜解析增強腳本 (雀魂/天鳳/麻雀一番街)
// @description:ja Mortal牌譜レビュー強化スクリプト (雀魂/天鳳(てんほう)/麻雀一番街)
// @description:ko Mortal 게임 기록 감사 강화 스크립트 (작혼/천봉/마작일번가)
// @description:ru Скрипт расширения аудита игровых записей Mortal (Mahjong Soul/Tenhou/Riichi City)
// @description:en Mortal game record review enhancement script (Mahjong Soul/Tenhou/Riichi City)
// @version      2.2.22.1
// @homepage     https://www.bilibili.com/read/cv26608482/
// @namespace    https://viayoo.com/
// @author       Miku39
// @icon         data:image/x-icon;base64,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5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+Zj9X/NR6//0Ywwv/Kxd//6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//0Iswv++uN3/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//z8owf+1rtv/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zwmwP+vptn/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zskwP+qodj/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zojwP+nntj/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+lnNf/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+lm9f/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiwP+kmtf/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fn5rwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5+fmvOjn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v+YjdT/NR6//zkiv/+jmtf/6Ofm/+jn5v/o5+b/6Ofm/+jn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5v/o5+b/6Ofm/+jn5v/n5uX+6Oblyf///wUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOTk5DDn5+bC5+fm/Ojn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+fm5f3o5+bD7unpLwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wHs7Owc5+fleOfl5Nbn5uX56Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/o5+b/6Ofm/+jn5v/n5uX56Ofl1efl5Xnj4+Mcf39/AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
// @resource     js_vue https://cdn.staticfile.org/vue/3.3.7/vue.global.min.js
// @resource     js_elementplus https://cdn.staticfile.org/element-plus/2.4.1/index.full.min.js
// @resource     js_layui https://cdn.staticfile.org/layui/2.8.17/layui.min.js
// @resource     js_cryptojs https://cdn.staticfile.org/crypto-js/4.1.1/crypto-js.min.js
// @resource     js_immutable https://cdn.staticfile.org/immutable/4.3.4/immutable.min.js
// @resource     js_localforage https://cdn.staticfile.org/localforage/1.10.0/localforage.min.js
// @resource     js_qunit https://cdn.staticfile.org/qunit/2.20.0/qunit.min.js
// @resource     css_elementplus https://cdn.staticfile.org/element-plus/2.4.1/index.min.css
// @resource     css_layui https://cdn.staticfile.org/layui/2.8.17/css/layui.min.css
// @resource     css_fontAwesome https://cdn.staticfile.org/font-awesome/6.4.2/css/all.min.css
// @match        *://mjai.ekyu.moe/*
// @grant        GM_info
// @grant        unsafeWindow
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_setClipboard
// @run-at       document-start
// @license      BSD-3-Clause
// ==/UserScript==

((g_window)=>{
    'use strict';
    //兼容浏览器翻译插件,比如谷歌翻译插件会更改lang属性(主要针对设置为自动翻译的语言)
    const g_origLang = document.scrollingElement.lang.toLocaleLowerCase();
    let g_currLang = g_origLang;
    const oHtml = document.getElementsByTagName("html")[0];
    const observer = new window.MutationObserver((mutations) => {
        g_currLang = oHtml.getAttribute("lang").toLocaleLowerCase();
        Console.orig.log(`原始语言:${g_origLang}, 当前被翻译为: ${g_currLang}`);
    });
    observer.observe(oHtml, { attributes: true, attributeFilter: ["lang"] });
    // ---------------------------- 全局变量: App变量 ----------------------------
    let globalSettings = { //存储永久数据
        Base: {
            appName: "Mortal",
            cssName: ".ui",
            curVersion: GM_info.script.version,
        },
        Config: {
            badMoveUpperLimit: 5, //恶手率
            badMoveUpperLimitCustom: 10, //恶手率
        },
        AppSettings: {
            global: {
                enableScrollAnimation: false, //启用滚动动画
            },
            loading: {
                isShowLoadingAnimation: true, //是否显示加载动画
                isShowLoadingProgress: true, //是否显示加载进度条
            },
            
        },
        UISettings: {
            basic: {
                global: {
                    fontColor: "#000", //字体颜色
                    currentTheme: "", //当前主题
                },
                pageExtend: {

                },
                MainApp: {
                    settingUI:{
                        window: {
                            x: 0, //posX
                            y: 0, //posY
                            z: 0, //z-index
                            width: "auto",
                            height: "auto",
                        },
                        cssStyle: "",
                    },
                    catalogUI: {
                        window: {
                            x: 0, //posX
                            y: 0, //posY
                            z: 0, //z-index
                            width: "auto",
                            height: "auto",
                        },
                        cssStyle: "",
                    },
                },
            },
            theme: [
                {
                    name: "暗色模式",
                },
            ]
        },
        Other: {
            debugMode: { //调试模式
                enable: false,
                trackCall: {
                    enable: false,
                    trackFunction: true,
                    trackTimestamp: true,
                },
                logOut: {
                    enable: true,
                    filter: "debug",
                }
            },
            safeMode: false, //安全模式 (禁用程序所有功能) //false
        },
    };

    const matchRule = {
        isInit: false,
        isRon3: true, //是否允许三家荣和
    };

    let globalStatus = { //存储临时数据
        globalExitConfirm: false, //全局退出确认 (比如更改了设置没有保存等情况下)
        openSettingUI: false,
    };

    const pageLanguage = g_origLang;
    const localization = {__proto__: null,
        zh_cn: Object.defineProperties({__proto__: null,
            badMove: "恶手",
            badMoveRatio: "恶手率",
            matchRatio: "AI 一致率",
            metaData: "元数据",
            //
            seatTypeA0: "东起",
            seatTypeA1: "南起",
            seatTypeA2: "西起",
            seatTypeA3: "北起",
            //
            seatTypeB0: "东家",
            seatTypeB1: "南家",
            seatTypeB2: "西家",
            seatTypeB3: "北家",
            //
            seatTypeC0: "自家",
            seatTypeC1: "下家",
            seatTypeC2: "对家",
            seatTypeC3: "上家",
            //
            seatStart: "亲家", //庄家
            //
            Ron: "荣和",
            Tsumo: '自摸',
            Ryuukyoku: '流局',
            RyuukyokuTsumo: "流局满贯",
            RyuukyokuType1: "九种九牌",
            //四风连打
            //四杠散了
            //四家立直
            //三家和了
            //
            badMoveError: "(恶手率统计只支持Mortal 3.1及更高版本,当前版本生成结果不可靠)",
            //
            badMoveUp: ` (严重错误 权重0~${globalSettings.Config.badMoveUpperLimit}%)`,
            badMoveDown: ` (普通错误 权重${globalSettings.Config.badMoveUpperLimit}~${globalSettings.Config.badMoveUpperLimitCustom}%)`,
            badMoveDiffer: "差值: ",
            //
            badMoveDiffer1: "微差(0~5): ",
            badMoveDiffer2: "小幅差距(5~10): ",
            badMoveDiffer3: "低等差距(10~20): ",
            badMoveDiffer4: "中等差距(20~40): ",
            badMoveDiffer5: "高等差距(40~60): ",
            badMoveDiffer6: "大幅度差距(60~80): ",
            badMoveDiffer7: "压倒性差距(80~100): ",
            //
            settingUIPanel: "设置面板",
            //
            kyokuSelector: "对局选择器",
            badChooseSelector: "恶手选择器",
            differSelector: "不一致选择器",
            openSetting: "打开设置 (等待完善)",
        },{
            getName: { //默认 不可修改,不可重新定义或者删除,不可枚举
                value : "zh-cn",
            },
        }),
        ja: Object.defineProperties({__proto__: null,
            badMove: "Bad move",
            badMoveRatio: "bad moves/total",
            matchRatio: "AI一致率",
            metaData: "メタデータ",
            //
            seatTypeA0: "東", //トン
            seatTypeA1: "南", //ナン
            seatTypeA2: "西", //シャー
            seatTypeA3: "北", //ペイ
            //
            seatTypeB0: "東家",
            seatTypeB1: "南家",
            seatTypeB2: "西家",
            seatTypeB3: "北家",
            //
            seatTypeC0: "自家",
            seatTypeC1: "下家",
            seatTypeC2: "対面",
            seatTypeC3: "上家",
            //
            seatStart: "親",
            //
            Ron: "ロン", //栄
            Tsumo: 'ツモ', //自摸
            Ryuukyoku: '流局',
            RyuukyokuTsumo: "流し満貫",
            RyuukyokuType1: "九種九牌",
            //四風連打
            //四開槓
            //四家立直
            //三家和 //トリロン
        },{
            getName: { //默认 不可修改,不可重新定义或者删除,不可枚举
                value : "ja",
            },
        }),
        ko: Object.defineProperties({__proto__: null,
            badMove: "Bad move",
            badMoveRatio: "bad moves/total",
            matchRatio: "matches/total",
            metaData: "메타데이터",
            //
            seatTypeA0: "동",
            seatTypeA1: "남",
            seatTypeA2: "서",
            seatTypeA3: "북",
            //
            seatTypeB0: "동",
            seatTypeB1: "남",
            seatTypeB2: "서",
            seatTypeB3: "북",
            //
            seatTypeC0: "나",
            seatTypeC1: "하가", //시모챠
            seatTypeC2: "대가", //대면 //또이멘
            seatTypeC3: "상가", //샹차
            //
            seatStart: "쫭찌아",
            //
            Ron: "론",
            Tsumo: '쯔모',
            Ryuukyoku: '유국',
            RyuukyokuTsumo: "유국만관",
            RyuukyokuType1: "구종구패",
            //사풍연타
            //사개깡
            //사가리치
            //삼가화
        },{
            getName: { //默认 不可修改,不可重新定义或者删除,不可枚举
                value : "ko",
            },
        }),
        en: Object.defineProperties({__proto__: null,
            badMove: "Bad move", //Bad Choose
            badMoveRatio: "bad moves/total", //Bad Choose Rate
            matchRatio: "matches/total",
            metaData: "Metadata",
            //
            modelTag: "model tag",
            //
            seatTypeA0: "East",
            seatTypeA1: "South",
            seatTypeA2: "West",
            seatTypeA3: "North",
            //
            seatTypeB0: "East",
            seatTypeB1: "South",
            seatTypeB2: "West",
            seatTypeB3: "North",
            //
            seatTypeC0: "Self", //Own
            seatTypeC1: "Shimocha", //Next player
            seatTypeC2: "Toimen", //Opposite player
            seatTypeC3: "Kamicha", //Last player
            //
            seatStart: "Oya", //Dealer
            //
            Ron: "Ron",
            Tsumo: 'Tsumo',
            Ryuukyoku: 'Ryuukyoku', //Exhaustive draw
            RyuukyokuTsumo: "Nagashi mangan", //Mangan at Draw
            RyuukyokuType1: "Kyuushu kyuuhai", //Nine Different Terminals and Honors
            //Suufon renda //Four-Wind Discarded
            //Suukaikan //Four-Kan Abortion
            //Suucha riichi //Four-Player Riichi
            //Sanchahou //Three-Player Ron
            //
            badMoveError: "(The Bad Choose Rate statistics are only supported by Mortal version 3.1 and later, the results generated by the current version are unreliable)",
            //
            badMoveUp: ` (Fatal Error: 0~${globalSettings.Config.badMoveUpperLimit}%)`,
            badMoveDown: ` (Error: ${globalSettings.Config.badMoveUpperLimit}~${globalSettings.Config.badMoveUpperLimitCustom}%)`,
            badMoveDiffer: "Differences: ",
            //
            badMoveDiffer1: "Very slight difference(0~5): ",
            badMoveDiffer2: "Small difference(5~10): ",
            badMoveDiffer3: "Low difference(10~20): ",
            badMoveDiffer4: "Medium difference(20~40): ",
            badMoveDiffer5: "Higher difference(40~60): ",
            badMoveDiffer6: "Wide difference(60~80): ",
            badMoveDiffer7: "Overwhelming difference(80~100): ",
            //
            settingUIPanel: "Settings Panel",
            //
            kyokuSelector: "Kyoku Selector",
            badChooseSelector: "Bad Choose Selector",
            differSelector: "Inconsistent Selector",
            openSetting: "Open Settings(unfinished)",
        },{
            getName: { //默认 不可修改,不可重新定义或者删除,不可枚举
                value: "en",
            },
        }),
    };
    let i18nText = { ...localization.en }; //复制对象
    if (pageLanguage == localization.zh_cn.getName) {
        Object.assign(i18nText, localization.zh_cn);
    } else if (pageLanguage == localization.ja.getName) {
        Object.assign(i18nText, localization.ja);
    } else if (pageLanguage == localization.ko.getName) {
        Object.assign(i18nText, localization.ko);
    }

    const tenhouText = {__proto__: null,
        Ron: '和了', //荣和
        Tsumo: '和了', //自摸
        Ryuukyoku: '流局', //荒牌流局
        RyuukyokuTsumo: "流し満貫", //流局满贯
        RyuukyokuType1: "九種九牌", //九种九牌
        //四風連打、四開槓、四家立直 //三家和
    };

    // ---------------------------- 全局变量: 模板 ----------------------------
    const cssID = globalSettings.Base.cssName;
    const cssIDNP = cssID.substring(1);

    const commonCSS = 
    '\
    .position_re { position: relative; }\
    .position_ab, .ui.position_ab { position: absolute; }\
    .position_fi { position: fixed; }\
    \
    .left-0 { left: 0px; }\
    .right-0 { right: 0px; }\
    .top-0 { top: 0px; }\
    .bottom-0 { bottom: 0px; }\
    .left-5 { left: 5px; }\
    .right-5 { right: 5px; }\
    .top-5 { top: 5px; }\
    .bottom-5 { bottom: 5px; }\
    .left-10 { left: 10px; }\
    .right-10 { right: 10px; }\
    .top-10 { top: 10px; }\
    .bottom-10 { bottom: 10px; }\
    \
    .text-left { text-align: left; }\
    .text-center { text-align: center; }\
    .text-right { text-align: right; }\
    \
    .font_weight_900 { font-weight: 900; }\
    .font_weight_700 { font-weight: 700; }\
    .font_weight_400 { font-weight: 400; }\
    \
    .w-auto { width: auto; }\
     .w-100 { width: 100%; }\
    .w-80 { width: 80%; }\
     .w-75 { width: 75%; }\
    .w-60 { width: 60%; }\
     .w-50 { width: 50%; }\
    .w-40 { width: 40%; }\
     .w-25 { width: 25%; }\
    .w-20 { width: 20%; }\
    .w-10 { width: 10%; }\
    .w-5  { width: 5%; }\
    \
    .min-w-auto { min-width: auto; }\
     .min-w-100 { min-width: 100%; }\
    .min-w-80 { min-width: 80%; }\
     .min-w-75 { min-width: 75%; }\
    .min-w-60 { min-width: 60%; }\
     .min-w-50 { min-width: 50%; }\
    .min-w-40 { min-width: 40%; }\
     .min-w-25 { min-width: 25%; }\
    .min-w-20 { min-width: 20%; }\
    .min-w-10 { min-width: 10%; }\
    .min-w-5  { min-width: 5%; }\
   \
    .max-w-auto { max-width: auto; }\
     .max-w-100 { max-width: 100%; }\
    .max-w-80 { max-width: 80%; }\
     .max-w-75 { max-width: 75%; }\
    .max-w-60 { max-width: 60%; }\
     .max-w-50 { max-width: 50%; }\
    .max-w-40 { max-width: 40%; }\
     .max-w-25 { max-width: 25%; }\
    .max-w-20 { max-width: 20%; }\
    .max-w-10 { max-width: 10%; }\
    .max-w-5  { max-width: 5%; }\
    \
    .cursor_default { cursor: default; }\
    .cursor_crosshair { cursor: crosshair; }\
    .cursor_pointer { cursor: pointer; }\
    .cursor_move { cursor: move; }\
    .cursor_text { cursor: text; }\
    .cursor_wait { cursor: wait; }\
    .cursor_help { cursor: help; }\
    \
    .alpha_1 { opacity: 1; }\
    .alpha_05 { opacity: 0.5; }\
    .alpha_0 { opacity: 0; }\
    \
    .show { visibility: visible; }\
    .hide { visibility: hidden; }\
    \
    .display_block { display: block; }\
    .display_inline { display: inline; }\
    .display_inlineblock { display: inline-block; }\
    .display_flex { display: flex; }\
    .display_inlineflex { display: inline-flex; }\
    .display_grid { display: grid; }\
    .display_inlinegrid { display: inline-grid; }\
    .display_none { display: none; }\
    .display_inherit { display: inherit; }\
    \
    .margin-0 { margin: 0px !important; }\
    .margin-right-2 { margin-right: 2px; }\
    .margin-right-5 { margin-right: 5px; }\
    .margin-right-10 { margin-right: 10px; }\
    .margin-right-20 { margin-right: 20px; }\
    \
    .padding-0 { padding: 0px !important; }\
    .padding-2 { padding: 2px; }\
    .padding-5 { padding: 5px; }\
    .padding-10 { padding: 10px; }\
    .padding-20 { padding: 20px; }\
    ';

    const pageExtendBaseCSS = 
    '\
    details.collapseEntryL1 { border: 2px solid #f00; }\
    details.collapseEntryL2 { border: 2px solid #6600FF; }\
    .badChoose { font-size: 20px; }\
    .level1 { color: #f00; font-weight: 900; }\
    .level2 { color: #6600FF; font-weight: 700; }\
    .color1 { color: #f00 !important; }\
    .color2 { color: #6600FF !important; }\
    .color3 { color: #FF0066 !important; }\
    .color4 { color: #990000 !important; }\
    .l-129px { left: 129px; }\
    .l-130px { left: 130px; }\
    .l-155px { left: 155px; }\
    .l-170px { left: 170px; }\
    .l-190px { left: 190px; }\
    .l-196px { left: 196px; }\
    .l-210px { left: 210px; }\
    .color5 { color: #CC0000 !important; }\
    .color6 { color: #333 !important; }\
    .bgColor1 { background: #FF0000A0 !important; }\
    .bgColor2 { background: #6600FFA0 !important; }\
    ';

    const MainAppBaseCSS = 
    '\
    html { scroll-behavior: auto; }\
    /*details { scroll-margin-top: calc(2em + 4px); }*/\
    .highlight { animation: shimmer 0.75s infinite; }\
      @keyframes shimmer {\
        0% { background: #ffd5d500; }\
        50% { background: #ffd5d5ff; }\
        100% { background: #ffd5d500; }\
      }\
    .badChoose_border { border: 1px solid #ccc; }\
    .badChoose_border_first { border-top: 1px solid #ccc; border-left: 1px solid #ccc; border-right: 1px solid #ccc; }\
    .badChoose_border_middle { border-left: 1px solid #ccc; border-right: 1px solid #ccc; }\
    .badChoose_border_last { border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; }\
    #loadingProgress { width: 100%; position: fixed; left: 0px; bottom: 0px; z-index: 100; }\
    ';

    const MainAppCSS = 
    '\
    .commonDIV { padding: 5px; }\
    #settingUI { width: 800px; height: 600px; border: 1px solid red; position: fixed; top: 100px; left: 100px; z-index: 10; }\
    #catalogUI { border: 1px solid blue; padding: 20px 0px 0px 0px; position: fixed; z-index: 10; }\
    #selectorGroups,#selectorGroups>* { width: 100%; overflow: auto; }\
    .ui.layui-nav-container>*:first-child { padding: 6px 0px 0px 0px; }\
    .ui.layui-nav-container>*:last-child { padding: 0px 0px 6px 0px; }\
    .ui.layui-nav-container>*:not(:first-child):not(:last-child) { padding: 0; }\
    ';

    //悬浮菜单(悬浮球)
    const appHoverFloatMenu = 
    `\
    `;

    //吸附菜单(贴边)
    const appAdsorptionMenu = 
    `\
    `;

    const loadingUITemplate = 
    `\
    <div class="${cssIDNP} layui-progress" lay-showPercent="true" id="loadingProgress" lay-filter="loading-filter-progress">\
        <div class="${cssIDNP} layui-progress-bar" lay-percent="5%"></div>\
    </div>\
    `;

    //右下角 公告/消息 显示区域
    const bottomRightShowBox = 
    `\
	<div id="bottomRightShowBox">\
	</div>\
    `;

    const settingUITemplate = 
    `\
    <div class="${cssIDNP} commonDIV" id="settingUI">\
        <div class="${cssIDNP} layui-card">\
            <div class="${cssIDNP} layui-card-header">${i18nText.settingUIPanel}</div>\
            <div class="${cssIDNP} layui-card-body">\
                <div class="${cssIDNP} layui-tab layui-tab-brief">\
                    <ul class="${cssIDNP} layui-tab-title">\
                        <li class="${cssIDNP} layui-this">功能设置</li>\
                        <li class="${cssIDNP}">UI设置</li>\
                        <li class="${cssIDNP}">资源包</li>\
                        <li class="${cssIDNP}">常见问题</li>\
                        <li class="${cssIDNP}">关于</li>\
                    </ul>\
                    <div class="${cssIDNP} layui-tab-content">\
                        <div class="${cssIDNP} layui-tab-item layui-show">\
                            <div>功能设置-1</div>\
                        </div>\
                        <div class="${cssIDNP} layui-tab-item">\
                            <div>UI设置-2</div>\
                        </div>\
                        <div class="${cssIDNP} layui-tab-item">\
                            <div>资源包-3</div>\
                        </div>\
                        <div class="${cssIDNP} layui-tab-item">\
                            <div>常见问题-4</div>\
                        </div>\
                        <div class="${cssIDNP} layui-tab-item">\
                            <div>关于-5</div>\
                        </div>\
                    </div>\
                </div>\
            </div>\
        </div>\
    </div>\
    `;

    const catalogUIBase = 
    `\
    <div class="${cssIDNP} commonDIV layui-bg-gray" id="catalogUI">\
    </div>\
    `;

    const catalogUITemplate = 
    `\
        <div class="${cssIDNP} layui-btn-container" id="catalogUIBuf">\
            <button type="button" class="${cssIDNP} layui-btn" id="but_kyoku_prev">\
                <i class="${cssIDNP} layui-icon layui-icon-prev"></i>\
            </button>\
            <button type="button" class="${cssIDNP} layui-btn" id="but_kyoku_next">\
                <i class="${cssIDNP} layui-icon layui-icon-next"></i>\
            </button>\
            <button type="button" class="${cssIDNP} layui-btn" id="but_diff_prev">\
                <i class="${cssIDNP} layui-icon layui-icon-left"></i>\
            </button>\
            <button type="button" class="${cssIDNP} layui-btn" id="but_diff_next">\
                <i class="${cssIDNP} layui-icon layui-icon-right"></i>\
            </button>\
        </div>\
        <div class="${cssIDNP} layui-nav-container layui-bg-gray" id="selectorGroups" data-scrollbar >\
            <ul class="${cssIDNP} layui-nav layui-nav-tree layui-bg-gray" lay-filter="selector-filter-nav">\
                <!-- ${i18nText.kyokuSelector} -->\
                <li class="${cssIDNP} layui-nav-item layui-nav-itemed">\
                    <a class="${cssIDNP} cursor_pointer" href="javascript:;">${i18nText.kyokuSelector}</a>\
                    <dl class="${cssIDNP} layui-nav-child" class="selector" id="selector1">\
                    {{#  layui.each(d.selector1, function(index, item){ }}\
                        <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP}" id="{{= item.nameID }}">{{= item.name }}</a></dd>\
                    {{#  }); }}\
                    </dl>\
                </li>\
            </ul>\
            <ul class="${cssIDNP} layui-nav layui-nav-tree layui-bg-gray" lay-filter="selector-filter-nav">\
                <!-- ${i18nText.badChooseSelector} -->\
                <li class="${cssIDNP} layui-nav-item">\
                    <a class="${cssIDNP} cursor_pointer" href="javascript:;">${i18nText.badChooseSelector}</a>\
                    <dl class="${cssIDNP} layui-nav-child" class="selector" id="selector2">\
                    {{#  layui.each(d.selector2, function(index, item){ }}\
                        {{#  if(item.badChooseType === 1){ }}
                            <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP} color1 font_weight_700" id="{{= item.nameID }}">{{= item.name }}</a></dd>\
                        {{#  }else{ }}
                            <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP} color2 font_weight_700" id="{{= item.nameID }}">{{= item.name }}</a></dd>\
                        {{#  } }}
                    {{#  }); }}\
                    </dl>\
                </li>\
            </ul>\
            <ul class="${cssIDNP} layui-nav layui-nav-tree layui-bg-gray" lay-filter="selector-filter-nav">\
                <!-- ${i18nText.differSelector} -->\
                <li class="${cssIDNP} layui-nav-item">\
                    <a class="${cssIDNP} cursor_pointer" href="javascript:;">${i18nText.differSelector}</a>\
                    <dl class="${cssIDNP} layui-nav-child" class="selector" id="selector3">\
                    {{#  layui.each(d.selector3, function(index, item){ }}\
                        {{#  if(item.badChooseType === 1){ }}
                            <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP} color1 font_weight_700" name="{{= item.nameID }}">{{= item.name }}</a></dd>\
                        {{#  }else if(item.badChooseType === 2){ }}
                            <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP} color2 font_weight_700" name="{{= item.nameID }}">{{= item.name }}</a></dd>\
                        {{#  }else{ }}
                            <dd class="${cssIDNP} cursor_pointer"><a class="${cssIDNP} font_weight_700" name="{{= item.nameID }}">{{= item.name }}</a></dd>\
                        {{#  } }}
                    {{#  }); }}\
                    </dl>\
                </li>\
            </ul>\
            <ul class="${cssIDNP} layui-nav layui-nav-tree layui-bg-gray position_ab bottom-0 padding-0" lay-filter="selector-filter-nav">\
                <li class="${cssIDNP} layui-nav-item text-center" style="border-top: 1px solid #ccc">\
                    <a class="${cssIDNP}" href="javascript:;">\
                    <i class="fa-solid fa-gear fa-lg margin-right-5"></i>${i18nText.openSetting}</a>\
                </li>\
            </ul>\
        </div>\
    `;

    // ---------------------------- 其他 ----------------------------
    // ---------------------------- 基本框架 ----------------------------

    const FILETYPE = {__proto__: null,
        JS: "js",
        CSS: "css",
    }

    const JSLOADTYPE = {__proto__: null,
        ASYNC: {__proto__: null, //异步加载
            name: "async",
            bit: 1,
        },
        DEFER: {__proto__: null, //延迟加载
            name: "defer",
            bit: 1 << 1,
        },
    }

    class TypeUtils {
        //
        static objType = '[object'; //对象类型
        //
        static undefinedType = '[object Undefined]'; //undefined类型
        static nullType = '[object Null]'; //null类型
        //
        static fnTypeArray = ['[object Function]', '[object AsyncFunction]']; //函数类型
        static strTypeArray = ['[object String]']; //字符串类型
        static symbolTypeArray = ['[object Symbol]']; //Symbol类型
        static arrayTypeArray = ['[object Array]']; //数组类型
        static numberTypeArray = ['[object Number]', '[object BigInt]']; //数字类型
        static booleanTypeArray = ['[object Boolean]']; //boolean类型
        static origObjType = ['[object Object]']; //原始对象类型
        //
        static mapType = ['[object Map]']; //Map对象类型
        static setType = ['[object Set]']; //Set对象类型
        //
        static dateType = ['[object Date]']; //Date对象类型
        static promiseType = ['[object Promise]']; //Promise对象类型
        static regExpType = ['[object RegExp]']; //RegExp对象类型
        //
        static errorType = ['[object Error]']; //Error对象类型 //Error、TypeError、SyntaxError、ReferenceError、RangeError、EvalError、URIError、InternalError
        //
        static getType(obj){
            return Object.prototype.toString.apply(obj);
        }
        static isValid(obj){
            if(obj == undefined || obj == null || Number.isNaN(obj)){
                return false;
            }
            if(TypeUtils.isString(obj)){
                if(TypeUtils.isUndefinedByType(obj) || TypeUtils.isNullByType(obj)){
                    return false;
                }
                return true;
            }
            return true;
        }
        static isValidValue(obj){
            if(TypeUtils.isValid(obj)){
                if(TypeUtils.isString(obj) || TypeUtils.isArray(obj)){
                    return obj.length > 0;
                }
                if(TypeUtils.isMap(obj) || TypeUtils.isSet(obj)){
                    return obj.size > 0;
                }
                return null;
            }
            return false;
        }
        // 判断是否为undefined
        static isUndefined(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isUndefinedByType(type);
        }
        static isUndefinedByType(obj){
            if(TypeUtils.undefinedType == obj) {
                return true;
            }
            return false;
        }
        // 判断是否为null
        static isNull(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isNullByType(type);
        }
        static isNullByType(obj){
            if(TypeUtils.nullType == obj) {
                return true;
            }
            return false;
        }
        // 判断是否为函数
        static isFunction(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isFunctionByType(type);
        }
        static isFunctionByType(obj){
            if(TypeUtils.fnTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        // 判断是否为字符串
        static isString(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isStringByType(type);
        }
        static isStringByType(obj){
            if(TypeUtils.strTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        // 判断是否为Symbol
        static isSymbol(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isSymbolByType(type);
        }
        static isSymbolByType(obj){
            if(TypeUtils.symbolTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为数组类型
        static isArray(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isArrayByType(type);
        }
        static isArrayByType(obj){
            if(TypeUtils.arrayTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为数字类型
        static isNumber(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isNumberByType(type);
        }
        static isNumberByType(obj){
            if(TypeUtils.numberTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为boolean类型
        static isBoolean(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isBooleanByType(type);
        }
        static isBooleanByType(obj){
            if(TypeUtils.booleanTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        // 判断是否为原始对象
        static isOrigObj(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isOrigObjByType(type);
        }
        static isOrigObjByType(obj){
            if(TypeUtils.origObjType.includes(obj)) {
                return true;
            }
            return false;
        }
        // 判断是否为对象(大范围)
        static isObject(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isObjectByType(type, obj);
        }
        static isObjectByType(obj, origObj){
            if(TypeUtils.isValid(origObj != undefined ? origObj : obj)){
                if(obj.startsWith(TypeUtils.objType)) {
                    return true;
                }
                return false;
            }
            return false;
        }
        //判断是否为Map类型
        static isMap(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isMapByType(type);
        }
        static isMapByType(obj){
            if(TypeUtils.mapType.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为Set类型
        static isSet(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isSetByType(type);
        }
        static isSetByType(obj){
            if(TypeUtils.setType.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为Date类型
        static isDate(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isDateByType(type);
        }
        static isDateByType(obj){
            if(TypeUtils.dateType.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为Promise类型
        static isPromise(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isPromiseByType(type);
        }
        static isPromiseByType(obj){
            if(TypeUtils.promiseType.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为RegExp类型
        static isRegExp(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isRegExpByType(type);
        }
        static isRegExpByType(obj){
            if(TypeUtils.regExpType.includes(obj)) {
                return true;
            }
            return false;
        }
        //判断是否为Error类型
        static isError(obj){
            let type = TypeUtils.getType(obj);
            return TypeUtils.isErrorByType(type);
        }
        static isErrorByType(obj){
            if(TypeUtils.errorType.includes(obj)) {
                return true;
            }
            return false;
        }
    }

    class DOMTypeUtils extends TypeUtils {
        static svgTypeArray = ['[object SVGSVGElement]']; //SVG节点类型
        static textTypeArray = ['[object Text]']; //TEXT节点类型
        // 判断是否为SVG节点
        static isSVGElement(obj){
            let type = DOMTypeUtils.getType(obj);
            return DOMTypeUtils.isSVGElementByType(type);
        }
        static isSVGElementByType(obj){
            if(DOMTypeUtils.svgTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }
        // 判断是否为Text节点
        static isTextElement(obj){
            let type = DOMTypeUtils.getType(obj);
            return DOMTypeUtils.isTextElementByType(type);
        }
        static isTextElementByType(obj){
            if(DOMTypeUtils.textTypeArray.includes(obj)) {
                return true;
            }
            return false;
        }

    }

    class Utils {
        static {
            Utils.setDnsPrefecth(true);
            Utils.addDnsPrefecthUrl("//cdn.staticfile.org");
            Utils.addDnsPrefecthUrl("//tenhou.net");
            Utils.addNewScript("js_immutable", GM_getResourceText("js_immutable"));
            Utils.addNewScript("js_cryptojs", GM_getResourceText("js_cryptojs"));
            //Utils.loadjscssFile(GM_getResourceURL("js_immutable"), FILETYPE.JS, JSLOADTYPE.ASYNC.bit | JSLOADTYPE.DEFER.bit);
        }

        static loadjscssFile(filePath, fileType, args_bits_LOADTYPE_bit) {
            let ele = undefined;
            if (fileType == FILETYPE.JS) {
                ele = document.createElement('script');
                ele.setAttribute("src", filePath);

                if((args_bits_LOADTYPE_bit & (1 << 0)) == 0 ? false : true){
                    ele.setAttribute(JSLOADTYPE.ASYNC.name, true);
                }else if((args_bits_LOADTYPE_bit & (1 << 1)) == 0 ? false : true){
                    ele.setAttribute(JSLOADTYPE.DEFER.name, true);
                }
            } else if (fileType == FILETYPE.CSS) {
                ele = document.createElement("link");
                ele.setAttribute("rel", "stylesheet");
                ele.setAttribute("href", filePath);
            }else{
                Console.orig.log(`不支持的文件类型: ${fileType}`);
                return;
            }
    
            if (ele != undefined) {
                document.getElementsByTagName("head")[0].appendChild(ele);
            }
        }

        static addNewStyle(id, newStyle) {
            let styleElement = document.getElementById(id);
            
            if (!styleElement) {
                styleElement = document.createElement('style');
                styleElement.id = id;
                document.getElementsByTagName('head')[0].appendChild(styleElement);
            }
            styleElement.appendChild(document.createTextNode(newStyle));
        }

        static addNewScript(id, newScript) {
            let scriptElement = document.getElementById(id);
    
            if (!scriptElement) {
                scriptElement = document.createElement('script');
                scriptElement.id = id;
                document.getElementsByTagName('head')[0].appendChild(scriptElement);
            }
            scriptElement.appendChild(document.createTextNode(newScript));
        }

        static addElementsByHTMLTemplateText(htmlTemplate, parentElement) {
            let ele = document.createElement("div");
            if(parentElement == undefined)
                document.body.appendChild(ele);
            else
                parentElement.appendChild(ele);
            ele.outerHTML = htmlTemplate;
        }

        static setDnsPrefecth(isEnable) {
            if(TypeUtils.isBoolean(isEnable)){
                if(isEnable){
                    isEnable = "on";
                }else{
                    isEnable = "off";
                }
            }else{
                isEnable = "on";
            }
            let ele = document.createElement("meta");
            ele.setAttribute("http-equiv", "x-dns-prefetch-control");
            ele.setAttribute("content", isEnable);
            document.getElementsByTagName("head")[0].appendChild(ele);
        }

        static addDnsPrefecthUrl(url){
            let ele = document.createElement("link");
            ele.setAttribute("rel", "dns-prefetch");
            ele.setAttribute("href", url);
            document.getElementsByTagName("head")[0].appendChild(ele);
        }
    }
    
    class CustomUtils {
        static{
            // 拓展系统库
            Math.constructor.roundEx = (nValue, n) => { //保留n位小数
                return Math.round(nValue*Math.pow(10,n))/Math.pow(10,n);
            }
            // 为JSON序列化添加对Map、Set类型的支持
            const { stringify, parse } = JSON
            JSON.stringify = function (value, replacer, space) {
            const _replacer =
                typeof replacer === 'function'
                    ? replacer
                    : function (_, value) {
                        return value
                    }
            replacer = function (key, value) {
                value = _replacer(key, value)
                if (value instanceof Set) value = `Set{${stringify([...value])}}`
                else if (value instanceof Map) value = `Map{${stringify([...value])}}`
                return value
                }
            return stringify(value, replacer, space)
            }
            JSON.parse = function (value, reviver) {
            if (!reviver)
                reviver = function (key, value) {
                if (/Set\{\[.*\]\}/.test(value))
                    value = new Set(parse(value.replace(/Set\{\[(.*)\]\}/, '[$1]')))
                else if (/Map\{\[.*\]\}/.test(value))
                    value = new Map(parse(value.replace(/Map\{\[(.*)\]\}/, '[$1]')))
                return value
                }
            return parse(value, reviver)
            }
        }

        static handleCSSCompatibility(idPrefix, cssText, ruleSet) { //处理css兼容性, 防止外部css库修改原始css样式, 并应用自定义规则
            let handleMode = 3; //默认为后置处理

            let retCssText = cssText;
            if(idPrefix != undefined && idPrefix != null && idPrefix != ""){ //安全检查: 是否处理名称空间
                let splitStrArray = cssText.split(/\{[\s\S]+?\}/g); //提取每一段的css选择器
                let startIndex = 0, endIndex = 0;
                splitStrArray.forEach((item,index)=> {
                    //从css选择器匹配中过滤不需要的元素
                    if(item.indexOf("@charset") != -1){
                        let start = item.indexOf("@charset");
                        let end = item.indexOf(';');
                        let array = item.split("");
                        array.splice(start, end-start +1);
                        item = array.join("");
                    }else if(item.indexOf("@import") != -1){
                        let start = item.indexOf("@import");
                        let end = item.indexOf(';');
                        let array = item.split("");
                        array.splice(start, end-start +1);
                        item = array.join("");
                    }

                    let cssSelectArray = item.match(/[A-Za-z0-9\:\-\_\@\.\#\+\>\<\*\?\;\!\[\]\=\"\'\`\/\\\(\)\{\}\~\^\$\|\%\&]+/g); //提取单个css选择器 //排除了,
                    //Console.orig.log("cssSelectArray: " + cssSelectArray);
                    
                    if(cssSelectArray != null) {
        
                        for (let i = 0; i < cssSelectArray.length; i++) {
                            const cssSelectStr = cssSelectArray[i];
                            handleMode = 3;
        
                            if(cssSelectStr.indexOf("@") != -1){ //需要排除的内容
                                break;
                            }
                            if(cssSelectStr == "from" || cssSelectStr == "to") //需要排除的内容
                                break;
                            if(cssSelectStr.indexOf("%") != -1) //需要排除的内容
                                break;
                            if(cssSelectStr.indexOf(":root") != -1){
                                handleMode = 2;
                            }else if(cssSelectStr.indexOf(":") != -1){ //需要排除的内容 (伪类元素选择器)
                                break;
                            }
        
                            //Console.orig.log("cssSelectArray: " + cssSelectArray);
                            startIndex = retCssText.indexOf(cssSelectStr, startIndex);
                            endIndex = startIndex + cssSelectStr.length;

                            if(handleMode == 1){ //前置处理, 如.ui.layui
                                retCssText = retCssText.substring(0, startIndex) + idPrefix + cssSelectStr + retCssText.substring(endIndex);
                            }else if(handleMode == 2){ //位于伪类之前处理
                                let pointIndex = cssSelectStr.indexOf(':');
                                let newCssSelectStr = cssSelectStr.substring(0, pointIndex -1) + idPrefix + cssSelectStr.substring(pointIndex);

                                retCssText = retCssText.substring(0, startIndex) + newCssSelectStr + retCssText.substring(endIndex);
                            }else{ //后置处理, 如p.layui.ui
                                retCssText = retCssText.substring(0, startIndex) + cssSelectStr + idPrefix + retCssText.substring(endIndex);
                            }
                            startIndex += idPrefix.length + 1 + cssSelectStr.length; //重新设置startIndex,防止重复查找
                            
                        }
                        startIndex = retCssText.indexOf("}", startIndex) + 1; //重新设置startIndex,防止从错误的位置开始查找
                        //Console.orig.log("\n" + retCssText);
                    }
                });
            }
            //根据规则进行替换
            if(ruleSet != undefined && ruleSet != null) {
                for (const [key, value] of ruleSet.entries()) {
                    retCssText = retCssText.replaceAll(key, value);
                }
            }
            //Console.orig.log("\n" + retCssText);
            return retCssText;
        }
    
        static handleJSCompatibility(idPrefix, jsText, ruleSet) { //处理js兼容性, 应用自定义规则
            //根据规则进行替换
            if(ruleSet != undefined && ruleSet != null) {
                for (const [key, value] of ruleSet.entries()) {
                    jsText = jsText.replaceAll(key, value);
                }
            }
            // Console.orig.log("\n" + jsText);
            return jsText;
        }

        static getTextLineNumByOffsetHeight(ele) {
            let styles = getComputedStyle(ele, null);
            let lineHeight = parseFloat(styles.lineHeight);
            let offsetHeight = parseFloat(ele.offsetHeight);
            let lineNum = offsetHeight / lineHeight;
            return Math.round(lineNum);
        }
        static getTextLineNum(ele) {
            let styles = getComputedStyle(ele, null);
            let lineHeight = parseFloat(styles.lineHeight);
            let height = parseFloat(styles.height);
            let offsetHeight = parseFloat(ele.offsetHeight);
            let lineNum = (height || offsetHeight) / lineHeight;
            return Math.round(lineNum);
        }
        static getStyleOfLineHeight(ele) {
            let styles = getComputedStyle(ele, null);
            let lineHeight = parseFloat(styles.lineHeight);
            return lineHeight;
        }

        static isShowInClientWindow(ele) {
            const scrollTop = document.scrollingElement.scrollTop;
            const clientHeight = document.scrollingElement.clientHeight;
            const scrollEndTop = scrollTop + clientHeight;
            if(ele.offsetTop + ele.offsetHeight >= scrollTop && ele.offsetTop <= scrollEndTop){
                return true;
            }else{
                return false;
            }
        }
        static isShowInClientWindowByApi(ele) {
            const clientHeight = document.scrollingElement.clientHeight;
            const {top, bottom} = ele.getBoundingClientRect();
            return bottom > 0 && top < clientHeight;
        }
        static isShowInClientWindowOfPositionMode(ele) {
            const positionMode = [
                "static",
                "relative",
                "absolute",
                "sticky",
                "fixed",
            ];
            const scrollTop = document.scrollingElement.scrollTop;
            const clientHeight = document.scrollingElement.clientHeight;
            const scrollEndTop = scrollTop + clientHeight;
            let parentEle = ele;
            let styles = getComputedStyle(parentEle, null);
            if(positionMode.includes(styles.position)){
                while(styles.position != "absolute" && styles.position != "fixed") { //循坏向上查找设置了绝对定位或固定定位的父节点
                    parentEle = parentEle.parentElement;
                    styles = getComputedStyle(parentEle, null);
                }
            }
            //以父节点为准
            const realTop = scrollTop + parentEle.offsetTop;
            const realBottom = realTop + parentEle.offsetHeight;
            if(realTop + parentEle.offsetHeight >= scrollTop && realTop <= scrollEndTop){
                return true;
            }else{
                return false;
            }
        }

        static isShowInClientWindowOfPositionModeAndScrollbar(ele) {
            // const positionMode = [
            //     "static",
            //     "relative",
            //     "absolute",
            //     "sticky",
            //     "fixed",
            // ];
            // // const scrollTop = document.scrollingElement.scrollTop;
            // // const clientHeight = document.scrollingElement.clientHeight;
            // // const scrollEndTop = scrollTop + clientHeight;
            // let parentEle = ele;
            // let styles = getComputedStyle(parentEle, null);
            // if(positionMode.includes(styles.position)){
            //     while(styles.position != "absolute" && styles.position != "fixed") { //循坏向上查找设置了绝对定位或固定定位的父节点
            //         parentEle = parentEle.parentElement;
            //         styles = getComputedStyle(parentEle, null);
            //     }
            // }
            // //以父节点为准
            // debugger
            // let scrollbarEle = parentEle.querySelector("[data-scrollbar]");
            // let scrollbarStyles = getComputedStyle(scrollbarEle, null);

            // const scrollTop = parentEle.offsetTop + scrollbarEle.offsetTop + scrollbarEle.scrollTop; //60+48=108
            // const clientHeight = scrollbarEle.clientHeight;
            // const scrollEndTop = scrollTop + clientHeight;

            // // const realTop = scrollTop + scrollbarEle.offsetTop;
            // // const realEndTop = realTop + scrollbarEle.offsetHeight;
            // //ele.offsetTop + ele.offsetHeight >= scrollTop && ele.offsetTop <= scrollEndTop
            // if(ele.parentElement.offsetTop + ele.parentElement.offsetHeight >= scrollTop && ele.parentElement.offsetTop <= scrollEndTop){
            //     return true;
            // }else{
            //     return false;
            // }
            return false;
        }


        static isShowInClientWindowOfNodeArray(eleArray, startIndex, endIndex) {
            if(eleArray == undefined || eleArray == null || eleArray.length == 0){
                return false;
            }
            if(eleArray.length == 1) {
                return CustomUtils.isShowInClientWindow(eleArray[0]);
            }
            if(startIndex == undefined || startIndex == null){
                startIndex = 0;
            }
            if(endIndex == undefined || endIndex == null){
                endIndex = eleArray.length - 1;
            }
            const scrollTop = document.scrollingElement.scrollTop;
            const clientHeight = document.scrollingElement.clientHeight;
            const scrollEndTop = scrollTop + clientHeight;

            if(eleArray[startIndex].offsetTop + ((eleArray[endIndex].offsetTop - eleArray[startIndex].offsetTop) + eleArray[endIndex].offsetHeight) >= scrollTop && eleArray[startIndex].offsetTop <= scrollEndTop){
                return true;
            }
            return false;
        }

        static getScrollTopByTargetEleOfHide(ele){ //2797
            return CustomUtils.getScrollTopByTargetEleOfShow(ele) + 1;
        }
        static getScrollTopByTargetEleOfShow(ele){ //显示 //2796
            return ele.offsetTop + ele.offsetHeight;
        }
        static getScrollBottomByTargetEleOfHide(ele){ //1523
            const clientHeight = document.scrollingElement.clientHeight;
            return ele.offsetTop - clientHeight;
        }
        static getScrollBottomByTargetEleOfShow(ele){ //显示 //1524
            return CustomUtils.getScrollBottomByTargetEleOfHide(ele) + 1;
        }

        static setScrollToTargetNode(targetNode, verticalAlign){
            // document.scrollingElement.scrollTop = targetNode.offsetTop;
            if(globalSettings.AppSettings.global.enableScrollAnimation){
                // 页面动画滚动过度 A标签高亮
                targetNode.scrollIntoView({
                    behavior: "smooth",
                    block: verticalAlign || "start",
                    inline: "start",
                });
            }else{
                //页面直接滚动不过度 A标签高亮
                targetNode.scrollIntoView({
                    behavior: "instant", //动画
                    block: verticalAlign || "start", //垂直对齐方式
                    inline: "start", //水平对齐方式
                });
            }
        }

        static setHighlightShow(targetNode, showTime_ms, EndCallBack = null, ...args){
            targetNode.classList.add("highlight");
            setTimeout(()=>{
                targetNode.classList.remove("highlight");
                if(EndCallBack != undefined && EndCallBack != null)
                    EndCallBack(args);
            }, showTime_ms);
        }

        //函数节流: 减少代码执行频率 (在一个单位时间内,只能触发一次函数)
        static throttle(fn, interval = 500) {
            let run = true;
          
            return function () {
                if (!run) return;
                run = false;
                setTimeout(() => {
                    fn.apply(this, arguments);
                    run = true;
                }, interval);
            };
        }
        //函数防抖: 判断某个动作结束,如滚动结束、input输入结束等 (在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时) (在规定时间内,只让最后一次生效,前面的不生效)
        static debounce(fn, interval = 500) {
            let timeout = null;
            
            return function () {
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    fn.apply(this, arguments);
                }, interval);
            };
        }

        static compareVersion(version1, version2) { //比较字符串,如果有非数字则忽略大小写
            if(version1 === version2){
                return 0; // 版本相同
            }
            const ver1Array = version1.match(/\d+/g).map(i => parseInt(i));
            const ver2Array = version2.match(/\d+/g).map(i => parseInt(i));
            const ver1Leng = ver1Array.length, ver2Leng = ver2Array.length;
            
            for (let i = 0; i < Math.max(ver1Leng, ver2Leng); i++) {
                const value1 = i < ver1Leng ? ver1Array[i] : 0;
                const value2 = i < ver2Leng ? ver2Array[i] : 0;
                if (value1 != value2) {
                    return value1 > value2 ? 1 : -1;
                }
            }
            //处理特殊版本, 比如带alpha、Beta标识的版本
            const newVersion1 = version1.replaceAll(/[(\d+)|(\.\-\_\\\/\|)|(\s)]/g, "").toLocaleLowerCase();
            const newVersion2 = version2.replaceAll(/[(\d+)|(\.\-\_\\\/\|)|(\s)]/g, "").toLocaleLowerCase();
            if(newVersion1 === newVersion2){
                return 0; // 版本相同
            }else{
                return -1; // 版本不同, 为了避免后续发生逻辑错误, 返回版本降级结果是最好的
            }
        }
    }

    class Config {

        static readConfig(){
        }
        static SaveConfig(){
        }
        static clear(){
        }
    }

    class DBConfig extends Config {

        static async readConfig(name){
            try {
                return await localforage.getItem(name);
            } catch (err) {
                Console.orig.error("获取数据时发生错误: \n" + err);
            }
        }
        static async SaveConfig(name, obj){
            const jsonStr = JSON.stringify(obj);
            try {
                await localforage.setItem(name, jsonStr);
            } catch (err) {
                Console.orig.error("存储数据时发生错误: \n" + err);
            }
        }

        static async clear(){ //重置数据库
            await localforage.clear();
        }
    }

    const windowInfo = {__proto__: null,
        x: 0,
        y: 0,
        l: 0,
        t: 0,
        isMouseDown: false,
    }

    class DomUtils {
        static windowInfoMap = new Map();
        static ObserverEleSetMap = new Map();

        static setMoveable(targetElement, isMove) {
            let id = targetElement.id;
            let winInfo = DomUtils.windowInfoMap.get(id);
            if(winInfo == undefined){
                winInfo = { ...windowInfo }; //复制对象
            }

            targetElement.addEventListener('mousedown', function(e){ //鼠标按下事件
                let idStr = e.target.id;
                //获取x坐标和y坐标
                winInfo.x = e.clientX;
                winInfo.y = e.clientY;
                
                //获取左部和顶部的偏移量
                winInfo.l = targetElement.offsetLeft;
                winInfo.t = targetElement.offsetTop;
                //开关打开
                winInfo.isMouseDown = true;
                //设置样式  
                targetElement.style.cursor = 'move';
            });
    
            targetElement.addEventListener('mousemove', function(e){ //鼠标移动
                if (winInfo.isMouseDown == false) {
                    return;
                }
                //获取x和y
                var nx = e.clientX;
                var ny = e.clientY;
                //计算移动后的左偏移量和顶部的偏移量
                var nl = nx - (winInfo.x - winInfo.l);
                var nt = ny - (winInfo.y - winInfo.t);
    
                targetElement.style.left = nl + 'px';
                targetElement.style.top = nt + 'px';
            });
    
            targetElement.addEventListener('mouseup', function(e){ //鼠标抬起事件
                //开关关闭
                winInfo.isMouseDown = false;
                targetElement.style.cursor = 'default';
            });

            DomUtils.windowInfoMap.set(id, winInfo);
        }

        static setNewActivateEle(targetEle, className) {
            if(targetEle != null && targetEle != undefined) {
                let groupID = targetEle.parentElement.parentElement.id;
                let ObserverEleArray = DomUtils.ObserverEleSetMap.get(groupID); //
                if(ObserverEleArray == undefined){
                    ObserverEleArray = [];
                }
                //清除旧数据
                for (let j = 0; j < ObserverEleArray.length; j++) {
                    const element = ObserverEleArray[j];
                    element.parentElement.classList.remove(className);
                }
                ObserverEleArray.length = 0; //清空数组
                
                //处理新数据
                targetEle.parentElement.classList.add(className);
                ObserverEleArray.push(targetEle);
                DomUtils.ObserverEleSetMap.set(groupID, ObserverEleArray); //
            }
        }
    }

    class MJCommonUtils {
        //标准局数 转 东南西场 (1=东 2=南 3=西)
        static normalKyokuToKyokuMode(curKyoku){
            let curkyokuMode; //东南西场 (1=东 2=南 3=西)

            if(curKyoku <= 3)
                curkyokuMode = 1; //东场
            else if(curKyoku <= 7)
                curkyokuMode = 2; //南场
            else if(curKyoku <= 11)
                curkyokuMode = 3; //西场

            return curkyokuMode;
        }
        //标准局数 转 文本局数 (0=>1: 东一) (6=>3: 南3)
        static normalKyokuToTextKyoku(curKyoku, curkyokuMode){
            let textKyoku = parseInt(curKyoku) + 1;
            if(curkyokuMode == 2) //南场
                textKyoku -= 4;
            else if(curkyokuMode == 3) //西场
                textKyoku -= 8;
            
            return textKyoku;
        }

        static getTextByOutStyle(outStyle, index) {
            if(outStyle == OUTSTYLE.A)
                return eval("i18nText.seatTypeA" + index); //seatTypeA0 //东起
            else if(outStyle == OUTSTYLE.B)
                return eval("i18nText.seatTypeB" + index); //seatTypeB0 //东家
            else if(outStyle == OUTSTYLE.C)
                return eval("i18nText.seatTypeC" + index); //seatTypeC0 //自家
            else{
                Console.orig.warn("outStyle是无效的!");
            }
        }
        
        static getSelfViewPlayerNameByTargetPlayerIndex(selfIndex, targetPlayerIndex) {
            const viewMap = new Map();

            let viewArray = [
                { value: -3, name: i18nText.seatTypeC1 }, //下家
                { value: -2, name: i18nText.seatTypeC2 }, //对家
                { value: -1, name: i18nText.seatTypeC3 }, //上家
                { value: 0, name: i18nText.seatTypeC0 },  //自家
                { value: 1, name: i18nText.seatTypeC1 },  //下家
                { value: 2, name: i18nText.seatTypeC2 },  //对家
                { value: 3, name: i18nText.seatTypeC3 },  //上家
            ];
            viewArray.forEach((item,index)=> {
                viewMap.set(item.value, item.name);
            });

            const getValue = (offset) => {
                if(offset > 0)
                    return offset -4;
                else
                    return offset +4;
            }
            
            let offset = targetPlayerIndex - selfIndex;
            let result;
            do {
                result = viewMap.get(offset);
            } while (offset = getValue(offset), result == undefined);
            return result;
        }
        static getPlayerIndexByPlayerSeatName(playerSeatName, outStyle) {
            let seatArray;
            if(outStyle == OUTSTYLE.A)
                seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA1, i18nText.seatTypeA2, i18nText.seatTypeA3]; //东起, 南起, 西起, 北起
            else if(outStyle == OUTSTYLE.B)
                seatArray = [i18nText.seatTypeB0, i18nText.seatTypeB1, i18nText.seatTypeB2, i18nText.seatTypeB3]; //东家, 南家, 西家, 北家
            else{
                Console.orig.warn("outStyle是无效的!");
                seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA1, i18nText.seatTypeA2, i18nText.seatTypeA3]; //东起, 南起, 西起, 北起
            }
            const seatMap = new Map();
            seatArray.forEach((item,index)=> {
                seatMap.set(item, index);
            });
            return seatMap.get(playerSeatName);
        }
        static getPlayerSeatNameByPlayerIndex(playerIndex, kyoku, outStyle) {
            let seatArray;
            if(outStyle == OUTSTYLE.A)
                seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA3, i18nText.seatTypeA2, i18nText.seatTypeA1]; //东起, 北起, 西起, 南起
            else if(outStyle == OUTSTYLE.B)
                seatArray = [i18nText.seatTypeB0, i18nText.seatTypeB3, i18nText.seatTypeB2, i18nText.seatTypeB1]; //东家, 北家, 西家, 南家
            else{
                Console.orig.warn("outStyle是无效的!");
                seatArray = [i18nText.seatTypeA0, i18nText.seatTypeA3, i18nText.seatTypeA2, i18nText.seatTypeA1]; //东起, 北起, 西起, 南起
            }
            const seatMap = new Map();
            seatArray.forEach((item,index)=> {
                seatMap.set(item, index);
            });
            const getValueByEachArray = (array, startIndex, eachCount) => {
                let length = array.length;
                let targetIndex = startIndex;
                for (let i = eachCount; i > 0; i--) {
                    if(++targetIndex >= length)
                        targetIndex = 0;
                }
                return array[targetIndex];
            }
            const getStart = (playerIndex, kyoku, outStyle) => {
                switch (playerIndex + kyoku) {
                    case 0:
                        return MJCommonUtils.getTextByOutStyle(outStyle, 0); //东起
                    case 1:
                        return MJCommonUtils.getTextByOutStyle(outStyle, 1); //南起
                    case 2:
                        return MJCommonUtils.getTextByOutStyle(outStyle, 2); //西起
                    case 3:
                        return MJCommonUtils.getTextByOutStyle(outStyle, 3); //北起
                    default:
                        return getStart(playerIndex, kyoku -4, outStyle);
                }
            }
            const get = (playerIndex, kyoku, outStyle) => {
                let startIndex = seatMap.get(getStart(playerIndex, 0, outStyle));
                return getValueByEachArray(seatArray, startIndex, kyoku);
            }
            if(kyoku == 0)
                return getStart(playerIndex, kyoku, outStyle);
            else
                return get(playerIndex, kyoku, outStyle);
        }

    }

    class MJUtils {

    }

    class MessageQueue {

    }

    class Popup {

    }

    class Api {
        static currApi;
        static commonApi = {
            name: "Tampermonkey",
            id: "",
            map: new Map([
                ["setValue", "GM_setValue"],          //
                ["getValue", "GM_getValue"],          //
                ["addStyle", "GM_addStyle"],
                ["deleteValue", "GM_deleteValue"],       //
                ["listValues", "GM_listValues"],        //
                ["addValueChangeListener", "GM_addValueChangeListener"],
                ["removeValueChangeListener", "GM_removeValueChangeListener"],
                ["log", "GM_log"],               //
                ["getResourceText", "GM_getResourceText"],   //
                ["getResourceURL", "GM_getResourceURL"],    //
                ["registerMenuCommand", "GM_registerMenuCommand"],
                ["unregisterMenuCommand", "GM_unregisterMenuCommand"],
                ["openInTab", "GM_openInTab"],         //
                ["xmlhttpRequest", "GM_xmlhttpRequest"],    //
                ["download", "GM_download"],          //
                ["getTab", "GM_getTab"],
                ["saveTab", "GM_saveTab"],
                ["getTabs", "GM_getTabs"],
                ["notification", "GM_notification"],
                ["setClipboard", "GM_setClipboard"],      //
                ["info", "GM_info"],
            ]),
        };
        // static viaApi = {
        //     name: "via",
        //     id: "via_gm",
        //     map: new Map([
        //         ["deleteValue", "deleteValue"],     //
        //         ["download", "download"],        //
        //         ["getResourceText", "getResourceText"], //
        //         ["getResourceURL", "getResourceURL"],  //
        //         ["getValue", "getValue"],        //
        //         ["isInstalled", "isInstalled"],
        //         ["listValues", "listValues"],      //
        //         ["log", "log"],             //
        //         ["openInTab", "openInTab"],       //
        //         ["openOptions", "openOptions"],
        //         ["setClipboard", "setClipboard"],    //
        //         ["setValue", "setValue"],        //
        //         ["xmlHttpRequest", "xmlHttpRequest"],  //
        //     ]),
        // };
        static{
            // if(Api.isDefine(Api.viaApi.id)){
            //     Api.currApi = Api.viaApi;
            // }else{
                Api.currApi = Api.commonApi;
            // }
        }
        static isDefine(id) {
            return eval(`typeof ${id} != 'undefined'`);
        }
        static getByMap(str) {
            let result = Api.currApi.map.get(str);
            return result != undefined ? result : Api.commonApi.map.get(str);
        }
        static commonCall(fnNameStr, ...args) {
            if(fnNameStr.startsWith("GM_"))
                return eval(fnNameStr + ".apply(this, args)");
            else if(Api.currApi.id == "")
                return eval(fnNameStr + ".apply(this, args)");
            else
                return eval(`${Api.currApi.id}.${fnNameStr}` + ".apply(this, args)");
        }
        //
        static GM_setValue(...args){ return Api.commonCall(Api.getByMap("setValue"), args); }
        static GM_getValue(...args){ return Api.commonCall(Api.getByMap("getValue"), args); }
        static GM_addStyle(...args){ return Api.commonCall(Api.getByMap("addStyle"), args); }
        static GM_deleteValue(...args){ return Api.commonCall(Api.getByMap("deleteValue"), args); }
        static GM_listValues(...args){ return Api.commonCall(Api.getByMap("listValues"), args); }
        static GM_addValueChangeListener(...args){ return Api.commonCall(Api.getByMap("addValueChangeListener"), args); }
        static GM_removeValueChangeListener(...args){ return Api.commonCall(Api.getByMap("removeValueChangeListener"), args); }
        static GM_log(...args){ return Api.commonCall(Api.getByMap("log"), args); }
        static GM_getResourceText(...args){ return Api.commonCall(Api.getByMap("getResourceText"), args); }
        static GM_getResourceURL(...args){ return Api.commonCall(Api.getByMap("getResourceURL"), args); }
        static GM_registerMenuCommand(...args){ return Api.commonCall(Api.getByMap("registerMenuCommand"), args); }
        static GM_unregisterMenuCommand(...args){ return Api.commonCall(Api.getByMap("unregisterMenuCommand"), args); }
        static GM_openInTab(...args){ return Api.commonCall(Api.getByMap("openInTab"), args); }
        static GM_xmlhttpRequest(...args){ return Api.commonCall(Api.getByMap("xmlhttpRequest"), args); }
        static GM_download(...args){ return Api.commonCall(Api.getByMap("download"), args); }
        static GM_getTab(...args){ return Api.commonCall(Api.getByMap("getTab"), args); }
        static GM_saveTab(...args){ return Api.commonCall(Api.getByMap("saveTab"), args); }
        static GM_getTabs(...args){ return Api.commonCall(Api.getByMap("getTabs"), args); }
        static GM_notification(...args){ return Api.commonCall(Api.getByMap("notification"), args); }
        static GM_setClipboard(...args){ return Api.commonCall(Api.getByMap("setClipboard"), args); }
        static GM_info(...args){ return Api.commonCall(Api.getByMap("info"), args); }
    }

    class Debug {
        static #bDebug = true; //调试模式
        static #bPublicApi = true;
        static {
            if(Debug.#bDebug && Debug.#bPublicApi){
                let execute = "";
                for (const [key, value] of Api.commonApi.map.entries()) {
                    execute += `if(typeof ${value} != 'undefined') g_window.${value} = ${value};\n`;
                }
                eval(execute);
            }
        }
        static set setDebug(debug){
            Debug.#bDebug = debug;
        }
        static get getDebug(){
            return Debug.#bDebug;
        }
        //
        static globalErrorHandle(errObj) {
            ElementPlus.ElNotification({
                title: errObj.name || 'Error',
                message: Vue.h('i', { style: 'color: red' }, errObj.toString()),
                type: 'error',
                duration: 4500,
            });
            Console.orig.error(errObj);
        }
        static globalOutputErrorHandle(errName, errStr) {
            ElementPlus.ElNotification({
                title: errName || 'Error',
                message: Vue.h('i', { style: 'color: red' }, errStr),
                type: 'error',
                duration: 4500,
            });
            Console.orig.error(errStr);
        }
        //
        
    }

    class Console {
        static orig = window.console; //保存原始对象
        static clear(){
            clear();
        }
        static log(...args){
            Debug.getDebug && 
            Console.orig.log('%c[log]', 'background: #ffa500; padding: 1px; color: #fff;', args);
        }
        static warn(...args){
            Debug.getDebug && 
            Console.orig.log('%c[warn]', 'background: #ffa500; padding: 1px; color: #fff;', args);
        }
        static error(...args){
            Debug.getDebug && 
            Console.orig.log('%c[error]', 'background: red; padding: 1px; color: #fff;', args);
        }
        static info(...args){
            Debug.getDebug && 
            Console.orig.log('%c[info]', 'background: #ffa500; padding: 1px; color: #fff;', args);
        }
        static table(...args){
            Debug.getDebug && 
            Console.orig.table(args);
        }
    }
    g_window.Console = Console;

    class Performance {
        constructor(name){
            if(name != undefined)
                this._name = name;
            else
                this._name = '';
            this.nTimeStart = 0;
            this.nTimeEnd = 0;
        }
        setStartTime() {
            this.nTimeStart = performance.now();
        }
        setEndTime() {
            this.nTimeEnd = performance.now();
        }
        getEndTime(fnName) {
            this.nTimeEnd = performance.now();
            let executionTime = this.nTimeEnd - this.nTimeStart;

            if(fnName == undefined || fnName == null){
                fnName = "";
            }else{
                fnName = `.${fnName}()`;
            }
            Console.orig.log(`${this._name}${fnName}代码执行时间: ${executionTime.toFixed(3)} 毫秒`); //
            return executionTime;
        }
    }

    const CallType = {__proto__: null,
        Positive: "Positive",
        Reverse: "Reverse",
    }
    const HookType = {__proto__: null,
        Single: "Single",
        Multi: "Multi",
    }
    class ProxyGenerator { //代理生成器: 生成代理方法
        proxyInfo = [
            CryptoJS.SHA1(this.generateSingle().toString().replace(/\s+/g, "")).toString(),
            CryptoJS.SHA1(this.generateMulti_ChainCall().toString().replace(/\s+/g, "")).toString(),
            CryptoJS.SHA1(this.generateMultiReverse_ChainCall().toString().replace(/\s+/g, "")).toString(),
        ];
        generateSingle(fcName_, origFcAddr_, newFcAddr_, newFcAddrLastCall_) { //单hook
            function _proxy_s(...args){
                let prototype = _proxy_s.prototype;
                if(prototype.hasOwnProperty("_fcData")) {
                    let fcData = _proxy_s.prototype._fcData;
                    let origArgs = [...args]; //复制数组
                    args.unshift(origFcAddr_); //添加到数组开头
                    let lastCallArgs = [...args]; //复制数组
                    if(fcData.newFcAddr.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                        args.splice(1, 0, undefined); //将undefined插入到数组第二个元素
                    let interceptor = fcData.newFcAddr.apply(this, args); //调用newFcAddr
                    if(interceptor == true || interceptor == undefined){
                        let ret = fcData.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
                        if(fcData.newFcAddrLastCall && fcData.newFcAddrLastCall.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                            lastCallArgs.splice(1, 0, ret); //将ret插入到数组第二个元素
                        if(fcData.newFcAddrLastCall)
                            fcData.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
                        return ret;
                    }else{
                        return; //否则就拦截
                    }
                }
            }
            _proxy_s.prototype._fcData = {
                fcName: fcName_ || "",
                origFcAddr: origFcAddr_ || null,
                newFcAddr: newFcAddr_ || null,
                newFcAddrLastCall: newFcAddrLastCall_ || null,
            };
            return _proxy_s;
        }
        generateMulti_OneToOneCall(This, key_) { //多层hook-正序-一对一调用
            function _proxy_m(...args){
                let prototype = _proxy_m.prototype;
                if(prototype.hasOwnProperty("_hk")) {
                    let hk = _proxy_m.prototype._hk;
                    let key = hk.key;
                    let hook = hk.obj;

                    let hookDataArray = hook.fcHookMap.get(key);
                    let ret;
                    let origArgs = [...args]; //复制数组
                    for (const [index, item] of hookDataArray.entries()) { //遍历多层hook
                        args.unshift(item.origFcAddr); //添加到数组开头
                        let lastCallArgs = [...args]; //复制数组
                        if(item.newFcAddr.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                            args.splice(1, 0, undefined); //将undefined插入到数组第二个元素
                        let interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
                        if(interceptor == true || interceptor == undefined){
                            ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
                            if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                                lastCallArgs.splice(1, 0, ret); //将ret插入到数组第二个元素
                            if(item.newFcAddrLastCall)
                                item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
                            if(index == hookDataArray.length -1){ //在多层hook结尾, 返回返回值
                                return ret;
                            }
                        }else{
                            return; //否则就拦截
                        }
                    }
                }
            }
            _proxy_m.prototype._hk = {
                key: key_ || "", 
                obj: This,
            };
            return _proxy_m;
        }
        generateMulti_ChainCall(This, key_) { //多层hook-正序-链式调用
            function _proxy_m(...args){
                let prototype = _proxy_m.prototype;
                if(prototype.hasOwnProperty("_hk")) {
                    let hk = _proxy_m.prototype._hk;
                    let key = hk.key;
                    let hook = hk.obj;

                    let hookDataArray = hook.fcHookMap.get(key);
                    let ret;
                    let origArgs = [...args]; //复制数组

                    let interceptor;
                    for (const [index, item] of hookDataArray.entries()) { //遍历多层hook
                        let args = [...origArgs]; //复制数组
                        args.unshift(item.origFcAddr); //添加到数组开头
                        let lastCallArgs = [...args]; //复制数组
                        if(item.newFcAddr.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                            args.splice(1, 0, undefined); //将undefined插入到数组第二个元素
                        interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
                        if(interceptor == true || interceptor == undefined){
                            if(index < hookDataArray.length -1){ //如果没有遍历完成,则继续链式调用
                                continue;
                            }else{
                                ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
                                if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                                    lastCallArgs.splice(1, 0, ret); //将ret插入到数组第二个元素
                                if(item.newFcAddrLastCall)
                                    item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
                                if(index == hookDataArray.length -1){ //在多层hook结尾, 返回返回值
                                    return ret;
                                }
                            }
                        }else{
                            return; //否则就拦截
                        }
                    }
                }
            }
            _proxy_m.prototype._hk = {
                key: key_ || "", 
                obj: This,
            };
            return _proxy_m;
        }
        generateMultiReverse_OneToOneCall(This, key_) { //多层hook-逆序-一对一调用
            function _proxy_mr(...args){
                let prototype = _proxy_mr.prototype;
                if(prototype.hasOwnProperty("_hk")) {
                    let hk = _proxy_mr.prototype._hk;
                    let key = hk.key;
                    let hook = hk.obj;

                    let hookDataArray = hook.fcHookMap.get(key);
                    let ret;
                    let origArgs = [...args]; //复制数组
                    for (let i = hookDataArray.length -1; i >= 0; i--) { //遍历多层hook
                        const item = hookDataArray[i];
                        args.unshift(item.origFcAddr); //添加到数组开头
                        let lastCallArgs = [...args]; //复制数组
                        if(item.newFcAddr.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                            args.splice(1, 0, undefined); //将undefined插入到数组第二个元素
                        let interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
                        if(interceptor == true || interceptor == undefined){
                            ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
                            if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                                lastCallArgs.splice(1, 0, ret); //将ret插入到数组第二个元素
                            if(item.newFcAddrLastCall)
                                item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
                            if(i == 0){ //在多层hook开头, 返回返回值
                                return ret;
                            }
                        }else{
                            return; //否则就拦截
                        }
                    }
                }
            }
            _proxy_mr.prototype._hk = {
                key: key_ || "", 
                obj: This,
            };
            return _proxy_mr;
        }
        generateMultiReverse_ChainCall(This, key_) { //多层hook-逆序-链式调用
            function _proxy_mr(...args){
                let prototype = _proxy_mr.prototype;
                if(prototype.hasOwnProperty("_hk")) {
                    let hk = _proxy_mr.prototype._hk;
                    let key = hk.key;
                    let hook = hk.obj;

                    let hookDataArray = hook.fcHookMap.get(key);
                    let ret;
                    let origArgs = [...args]; //复制数组
                    for (let i = hookDataArray.length -1; i >= 0; i--) { //遍历多层hook
                        let args = [...origArgs]; //复制数组
                        const item = hookDataArray[i];
                        args.unshift(item.origFcAddr); //添加到数组开头
                        let lastCallArgs = [...args]; //复制数组
                        if(item.newFcAddr.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                            args.splice(1, 0, undefined);; //将undefined插入到数组第二个元素
                        let interceptor = item.newFcAddr.apply(this, args); //调用newFcAddr
                        if(interceptor == true || interceptor == undefined){
                            if(i > 0){ //如果没有遍历完成,则继续链式调用
                                continue;
                            }else{
                                ret = item.origFcAddr.apply(this, origArgs); //调用原函数 //this是调用方传递的,也一并传递
                                if(item.newFcAddrLastCall && item.newFcAddrLastCall.length == 2) //根据模板函数实际参数个数,传入匹配的参数
                                    lastCallArgs.splice(1, 0, ret); //将ret插入到数组第二个元素
                                if(item.newFcAddrLastCall)
                                    item.newFcAddrLastCall.apply(this, lastCallArgs); //调用newFcAddrLastCall
                                if(i == 0){ //在多层hook开头, 返回返回值
                                    return ret;
                                }
                            }
                        }else{
                            return; //否则就拦截
                        }
                    }
                }
            }
            _proxy_mr.prototype._hk = {
                key: key_ || "", 
                obj: This,
            };
            return _proxy_mr;
        }
    }

    class Hook {
        #proxyGenerator = new ProxyGenerator();
        #callType = CallType.Positive;
        constructor(proxyGenerator){
            if(proxyGenerator != undefined)
                this.setProxyGenerator = proxyGenerator;
            this.fcHookMap = new Map();
            //this.objHookMap = new Map();
        }
        set setProxyGenerator(proxyGenerator) {
            this.#proxyGenerator = proxyGenerator;
        }
        setCallType(callType) {
            this.#callType = callType;
            for (const [key, item] of this.fcHookMap) { //遍历map
                if(item.length > 1){ //排除单hook,因为单hook没有顺序区别
                    if(callType == CallType.Positive){
                        eval(`${key}=this.#proxyGenerator.generateMulti_ChainCall(this, '${key}')`); //替换
                        return true;
                    }else if(callType == CallType.Reverse){
                        eval(`${key}=this.#proxyGenerator.generateMultiReverse_ChainCall(this, '${key}')`); //替换
                        return true;
                    }else{
                        return false;
                    }
                }
            }
        }
        get getCallType(){
            return this.#callType;
        }
        //设置hook时,设置的是代理函数,然后在代理函数里调用newFcAddr
        //流程: 其他代码调用 fcAddr =>(实际调用) 代理函数 ->(里面先拿到原函数) 再调用newFcAddr ->(结束后根据返回值如果为true则调用原函数,并返回返回值,false就拦截)
        //如果newFcAddr没有返回值 (说明不使用拦截功能),就正常调用原函数,返回返回值
        //代理函数是动态生成的,并且代理函数里不能调用被hook的函数,不然会无限递归调用
        setSingleHook(fcAddrStr, newFcAddr, newFcAddrLastCall){
            if(!TypeUtils.isString(fcAddrStr)){
                Console.orig.log("setSingleHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
                return false;
            }
            let fcAddr = eval(fcAddrStr);
            let type = TypeUtils.getType(fcAddr);
            let name = fcAddr.name;
            let key = fcAddrStr;
            if(TypeUtils.isFunction(fcAddr)){
                if(!this.fcHookMap.has(key)){
                    Console.orig.log("新fcHook: " + key);
                    this.fcHookMap.set(key, [{
                        fcName: name,
                        origFcAddr: fcAddr,
                        newFcAddr: newFcAddr,
                        newFcAddrLastCall: newFcAddrLastCall,
                    }]); //保存原始
                    eval(`${fcAddrStr}=this.#proxyGenerator.generateSingle('${name}', fcAddr, newFcAddr, newFcAddrLastCall)`); //替换
                    return true;
                }else{
                    //先取消hook,再重新单hook
                    let hookDataArray = this.fcHookMap.get(key);
                    this.unHookByNameOfSpecifyOrAll(fcAddrStr, hookDataArray[0].newFcAddr);
                    return this.setSingleHook(fcAddrStr, newFcAddr, newFcAddrLastCall);
                }
            }else{
                Console.orig.log("setSingleHook()失败, 不支持的type: " + type);
                return false;
            }
        }
        setHook(fcAddrStr, newFcAddr, newFcAddrLastCall){
            if(!TypeUtils.isString(fcAddrStr)){
                Console.orig.log("setHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
                return false;
            }
            let fcAddr = eval(fcAddrStr);
            let type = TypeUtils.getType(fcAddr);
            let name = fcAddr.name;
            let key = fcAddrStr;
            if(TypeUtils.isFunction(type)){
                if(!this.fcHookMap.has(key)){
                    Console.orig.log("新fcHook: " + key);
                    this.fcHookMap.set(key, [{
                        fcName: name,
                        origFcAddr: fcAddr,
                        newFcAddr: newFcAddr,
                        newFcAddrLastCall: newFcAddrLastCall,
                    }]); //保存原始
                    eval(`${fcAddrStr}=this.#proxyGenerator.generateSingle('${name}', fcAddr, newFcAddr, newFcAddrLastCall)`); //替换
                    return true;
                }else{ //多层Hook
                    Console.orig.log("多层fcHook: " + key);
                    let hookDataArray = this.fcHookMap.get(key);
                    hookDataArray.push({
                        fcName: name,
                        origFcAddr: hookDataArray[0].origFcAddr,
                        newFcAddr: newFcAddr,
                        newFcAddrLastCall: newFcAddrLastCall,
                    }); //保存原始
                    this.fcHookMap.set(key, hookDataArray);
                    if(this.#callType == CallType.Positive){
                        eval(`${fcAddrStr}=this.#proxyGenerator.generateMulti_ChainCall(this, '${key}')`); //替换
                        return true;
                    }else if(this.#callType == CallType.Reverse){
                        eval(`${fcAddrStr}=this.#proxyGenerator.generateMultiReverse_ChainCall(this, '${key}')`); //替换
                        return true;
                    }
                }
            }else{
                Console.orig.log("setHook()失败, 不支持的type: " + type);
                return false;
            }
        }
        isHook(fcAddrStr){
            if(!TypeUtils.isString(fcAddrStr)){
                Console.orig.log("isHook()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
                return false;
            }
            if(this.fcHookMap.has(fcAddrStr)){
                return true;
            }else{
                let fcAddr = eval(fcAddrStr);
                if(fcAddr == undefined){
                    return false;
                }
                let hash = CryptoJS.SHA1(fcAddr.toString().replace(/\s+/g, "")).toString();
                for (const [index, item] of this.#proxyGenerator.proxyInfo.entries()) {
                    if(item == hash) {
                        return true;
                    }
                }
                return false;
            }
        }
        getHookType(fcAddrStr){
            if(!TypeUtils.isString(fcAddrStr)){
                Console.orig.log("getHookType()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
                return false;
            }
            const hookDataArray = this.fcHookMap.get(fcAddrStr);
            if(hookDataArray != undefined) {
                return hookDataArray.length == 1 ? HookType.Single : HookType.Multi;
            }else{
                return null;
            }
        }
        #unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i){
            if(hookDataArray.length == 1){ //单hook 或 只有1个元素
                eval(`${fcAddrStr}=hookData.origFcAddr`); //还原
                this.fcHookMap.delete(fcAddrStr);
                return true;
            }else{ //多层
                hookDataArray.splice(i, 1); //删除当前记录
                this.fcHookMap.set(fcAddrStr, hookDataArray);
                return true;
            }
        }
        #unHookCommon_mode(fcAddrStr, newFcAddr, hookData, hookDataArray, i){
            if(newFcAddr != undefined && hookData.newFcAddr == newFcAddr){ //unHookByName //取消指定key某一层hook
                return this.#unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i);
            }else if(newFcAddr == undefined){ //unAllHookByName //取消指定key下的所有hook
                return this.#unHookCommon_unload(fcAddrStr, hookData, hookDataArray, i);
            }
        }
        //取消hook, 主要针对多层hook
        //unHook(key, 新函数地址)
        //unHook(key)
        //如果指定newFcAddr, 则取消指定key某一层hook
        //如果只指定fcAddrStr, 则取消指定key下的所有hook
        unHookByNameOfSpecifyOrAll(fcAddrStr, newFcAddr){
            if(!TypeUtils.isString(fcAddrStr)){
                Console.orig.log("unHookByNameOfSpecifyOrAll()失败, fcAddrStr参数错误, 应该是字符串! 实际是: " + fcAddrStr);
                return false;
            }
            if(this.isHook(fcAddrStr)){
                let fcAddr = eval(fcAddrStr);
                let type = TypeUtils.getType(fcAddr);
                let key = fcAddrStr;
                if(TypeUtils.isFunction(type)){
                    if(this.fcHookMap.has(key)){
                        let hookDataArray = this.fcHookMap.get(key);
                        for (let i = hookDataArray.length -1; i >= 0; i--) { //逆序遍历
                            const hookData = hookDataArray[i];
                            let ret = this.#unHookCommon_mode(fcAddrStr, newFcAddr, hookData, hookDataArray, i);
                            if(newFcAddr != undefined && ret){ //单unHook, 并且执行成功, 则直接返回
                                return true;
                            }else if(newFcAddr == undefined && i == 0){ //否则是 取消指定key下的所有hook, 就判断是否循坏完成, 循坏完成后返回
                                return true;
                            }
                        }
                        Console.orig.log("unHook()失败, 找到hook, 但是实参newFcAddr有误: " + newFcAddr.name);
                        return false;
                    }else{
                        Console.orig.log("unHook()失败, 找到hook, 但是实参key有误: " + key);
                        return false;
                    }
                }else{
                    Console.orig.log("unHook()失败, 不支持的type: " + type);
                    return false;
                }
            }else{
                Console.orig.log("unHook()失败, 找不到指定的hook!");
                return false;
            }
        }
        unAllHook(){ //取消所有
            for (const [key, hookDataArray] of this.fcHookMap) { //遍历map
                for (let i = hookDataArray.length -1; i >= 0; i--) { //逆序遍历
                    const hookData = hookDataArray[i];
                    this.#unHookCommon_mode(key, undefined, hookData, hookDataArray, i);
                }
            }
            return true;
        }
    }

    class DynamicProxy {
        //DynamicProxy.apply(mainObj.perfor, "getEndTime"); //间接调用
        static apply(obj, fcName, ...args) { //执行目标对象的方法(对象, 方法名, 方法参数)
            let prototype = Object.getPrototypeOf(obj);
            if(prototype.hasOwnProperty(fcName)) {
                return eval("prototype." + fcName).apply(obj, args);
            }
        }
        static autoWired(mainObj, targetClass, CallBack) { //依赖注入
            for (const [name, value] of Object.entries(mainObj)) { //遍历对象获取指定类型的属性, 属性的类型通过参数传入
                if(value instanceof targetClass){
                    return CallBack(name, value); //CallBack(属性名, 属性值)
                }
            }
            return null;
        }
    }

    class CodeTemplate {
        static hook = new Hook();
        static autoWiredProxy(mainObj, executeFcName){ //依赖注入代理
            return DynamicProxy.autoWired(mainObj, Performance, (name, value)=>{
                //使用这个属性调用apply实现具体的功能
                DynamicProxy.apply(value, executeFcName); //apply(属性值(对象), 要执行的方法)
                return true;
            });
        }
        static autoWired_Performance(fcAddrStr, mainObj){
            if(CodeTemplate.hook.isHook(fcAddrStr)){
                Console.warn(`${fcAddrStr}已注入${mainObj.name}, 请勿重复调用!`);
                return false;
            }
            const fcStartStub = (origFcAddr, ...args)=>{
                if(CodeTemplate.autoWiredProxy(mainObj, "setStartTime") == null){ //依赖注入
                    Debug.globalOutputErrorHandle("autoWiredProxy()失败!", "返回值为null.");
                }
            };
            const fcEndStub = (origFcAddr, ret, ...args)=>{
                if(CodeTemplate.autoWiredProxy(mainObj, "getEndTime") == null){ //依赖注入
                    Debug.globalOutputErrorHandle("autoWiredProxy()失败!", "返回值为null.");
                }
            };
            CodeTemplate.hook.setSingleHook(fcAddrStr, fcStartStub, fcEndStub);
        }
    }

    const URLTYPE = {__proto__: null,
        LINK: 1, //链接
        FUZZYMATCH: 1 << 1, //模糊匹配
        RegExp: 1 << 2, //正则表达式
    }

    class URL {
        static test(testURL, urlTYPE, urlRule) {
            if(urlTYPE == URLTYPE.LINK){
                return urlRule.includes(testURL); //子串
            }else if(urlTYPE == URLTYPE.FUZZYMATCH){
                let targetURL = urlRule;
                //转义可能的特殊字符 //\和/这种字符必须首先执行 //*和?这种通配符放在后面单独处理
                let specharsArray = ['\\', '/', '[', ']', '(', ')', '{', '}', '^', '$', '-', '.', '+', '|', ',', ':', '=', '!', '<', '%'];
                specharsArray.forEach((item,index)=> {
                    targetURL = targetURL.replaceAll(item, '\\' + item);
                });
                //替换通配符 //?必须比*先执行
                targetURL = targetURL.replaceAll("?", "[\\s\\S]?");
                targetURL = targetURL.replaceAll("*", "([\\s\\S]+)?");
                //测试URL
                let regExp = new RegExp(targetURL);
                return regExp.test(testURL);
            }else if(urlTYPE == URLTYPE.RegExp){
                return urlRule.test(testURL);
            }
        }
    }

    class PageCallBack {
        static #css_layui_ruleSet = new Map([
            //重定向字体文件
            ["url(../", "url(https://cdn.staticfile.org/layui/2.8.17/"]
        ]);
        static #css_fontAwesome_ruleSet = new Map([
            //重定向字体文件(这里有些带",有些不带")
            ['url("../', 'url("https://cdn.staticfile.org/font-awesome/6.4.2/'],
            ['url(../', 'url(https://cdn.staticfile.org/font-awesome/6.4.2/']
        ]);
        static #js_layui_ruleSet = new Map([
            //解决动态添加的节点与css兼容性的问题
            ['class="', `class="${cssIDNP} `]
        ]);
        //---------- 通用 ----------
        static init(){
            //加载库-css
            Utils.addNewStyle("css_fontAwesome", CustomUtils.handleCSSCompatibility(null, GM_getResourceText("css_fontAwesome"), PageCallBack.#css_fontAwesome_ruleSet));
            Utils.addNewStyle("css_layui", CustomUtils.handleCSSCompatibility(cssID, GM_getResourceText("css_layui"), PageCallBack.#css_layui_ruleSet));
            Utils.addNewStyle("css_elementplus", GM_getResourceText("css_elementplus"));

            //加载自定义-css
            Utils.addNewStyle("commonCSS", commonCSS);
            Utils.addNewStyle("pageExtendBaseCSS", pageExtendBaseCSS);
            Utils.addNewStyle("MainAppBaseCSS", MainAppBaseCSS);
            Utils.addNewStyle("MainAppCSS", MainAppCSS);

            //加载库-js
            Utils.addNewScript("js_localforage", GM_getResourceText("js_localforage"));
            Utils.addNewScript("js_layui", CustomUtils.handleJSCompatibility(cssID, GM_getResourceText("js_layui"), PageCallBack.#js_layui_ruleSet));
            Utils.addNewScript("js_vue",  GM_getResourceText("js_vue"));
            Utils.addNewScript("js_elementplus", GM_getResourceText("js_elementplus"));

            //预初始化
            UI.init();
        }
        static async ready() {
            //执行初始化
            localforage.config({
                name: 'MortalApp',
                storeName: 'AppDB',
                version: '1.0',
                description: 'default database',
            });

            //读取设置
            let jsonStr = await DBConfig.readConfig("globalSettings"); //从数据库中读取配置
            if(jsonStr != null) {
                let obj = JSON.parse(jsonStr);
                if(TypeUtils.isOrigObjByType(obj)){
                    let result = CustomUtils.compareVersion(globalSettings.BaseInfo.curVersion, obj.BaseInfo.curVersion);
                    if(result == 1){ //如果更新了新版本
                        //优先迁移数据库
                        Console.orig.log("版本更新, 尝试迁移数据库...");

                        //迁移失败时, 重置数据库
                        Console.orig.log("版本更新, 数据库迁移失败! 需要重置数据库...");
                        await DBConfig.clear(); //重置数据库
                    }else if(result == -1){ //如果回退了旧版本
                        //优先迁移数据库
                        Console.orig.log("版本回退, 尝试迁移数据库...");

                        //迁移失败时, 重置数据库
                        Console.orig.log("版本回退, 数据库迁移失败! 需要重置数据库...");
                        await DBConfig.clear(); //重置数据库
                    }else{
                        globalSettings = obj;
                        Console.orig.log("已读取设置: globalSettings");
                    }
                }else{
                    Console.orig.error("无法识别数据库中的数据: globalSettings, 需要重置数据库...");
                    await DBConfig.clear(); //重置数据库
                }
            }else{
                Console.orig.log("数据库中没有 globalSettings 数据!");
            }
        }

        static async end() {
            UI.setValueByLoadingProgress(100);
            UI.setShowLoadingProgress(false);
            UI.setShowLoadingAnimation(false);
        }
    }

    class ReportCallBack extends PageCallBack {
        //---------- 详细页 ----------
        static async init() {
            super.init();

            //加载模板
            // Utils.addElementsByHTMLTemplateText(bottomRightShowBox);
            // Utils.addElementsByHTMLTemplateText(settingUITemplate);
            Utils.addElementsByHTMLTemplateText(catalogUIBase);

            return await ReportCallBack.ready();
        }
        static async ready() {
            await super.ready();
            PageExtend.init();
            MainApp.init();

            UI.setValueByLoadingProgress(10);
            return true;
        }
        static async run() {
            if(!globalSettings.Other.safeMode) {
                await PageExtend.run();
                await MainApp.run();
            }
            return await ReportCallBack.end();
        }
        static async end() {
            await super.end();
            return true;
        }
    }

    class MainPageCallBack extends PageCallBack {
        //---------- 主页 ----------
        static async init() {
            super.init();
            
            //加载模板


            return await MainPageCallBack.ready();
        }
        static async ready() {
            await super.ready();

            UI.setValueByLoadingProgress(10);
            return true;
        }
        static async run() {
            if(!globalSettings.Other.safeMode) {
                //修复原页面bug: 在主页输入一番街牌谱或自定义牌谱后进入牌谱解析页面,再后退回到主页时,不显示对应的输入框
                let radioEle = document.querySelectorAll('[type="radio"][name="input-method"]');
                for (let i = 0; i < radioEle.length; i++) {
                    const ele = radioEle[i];
                    if(ele.checked){
                        ele.onchange();
                        break;
                    }
                }

                document.getElementsByName("show-rating").forEach((ele)=>{ele.checked = true}); //默认勾选 显示Rating

                const map = new Map();
                let childEle = document.getElementById("mortal-model-tag").children;
                for (let i = 0; i < childEle.length; i++) {
                    const ele = childEle[i];
                    map.set(ele.value, ele.innerText); //将数据保存到map
                }
                const jsonStr = JSON.stringify(Object.fromEntries(map));
                try {
                    // 存储不同语言的 Mortal 版本
                    const pageLanguage = g_origLang;
                    let allMap;
                    let allJsonStr = await localforage.getItem("Mortal_All");
                    if(allJsonStr == null) {
                        allMap = new Map();
                    } else{
                        allMap = new Map(Object.entries(JSON.parse(allJsonStr)));
                    }
                    allMap.set(pageLanguage, map);
                    allJsonStr = JSON.stringify(Object.fromEntries(allMap));

                    //牌谱解析页面,默认使用最新的Mortal
                    await localforage.setItem("Mortal_New", childEle[0].value);
                    await localforage.setItem("Mortal_Type", jsonStr);
                    await localforage.setItem("Mortal_All", allJsonStr);
                } catch (err) {
                    Console.orig.error("存储数据时发生错误: \n" + err);
                }
            }
            return await MainPageCallBack.end();
        }
        static async end() {
            await super.end();
            return true;
        }
    }

    class App {
        static perfor = new Performance("dom");
        static URLRuleSet = [{
                name: "牌谱解析页面",
                url: /^https?:\/\/mjai.ekyu.moe\/report\/[A-Za-z0-9-_]+.html/,
                urlType: URLTYPE.RegExp,
                exclude: [
                ],
                usabilityTest: [
                    "document.body.children.length <= 5 || document.body.firstChild.tagName == 'PRE'",
                ],
                init: ReportCallBack.init,
                entry: ReportCallBack.run,
            },{
                name: "主页",
                url: /^https?:\/\/mjai.ekyu.moe\/?([A-Za-z0-9-_]+.html)?/,
                urlType: URLTYPE.RegExp,
                exclude: [
                    //排除主动访问非页面URL的内容,比如图片等
                    /^https?:\/\/mjai.ekyu.moe\/?([A-Za-z0-9-_]+)?\/?[A-Za-z0-9-_]+.(?!.*(html|htm|jsp|php|asp))/,
                ],
                usabilityTest: [
                ],
                init: MainPageCallBack.init,
                entry: MainPageCallBack.run,
            },
        ]; //URL规则集
        constructor(){
            this.isRun = false;
            this.init();
        }
        async init() {
            document.addEventListener('readystatechange', async (event) => {
                if (event.target.readyState === 'interactive') {
                    App.perfor.setStartTime();
                    //Console.orig.log('interactive');
                    await this.yun(); //chrome第一次在这里触发
                    //initLoader();
                }
                else if (event.target.readyState === 'complete') {
                    App.perfor.getEndTime();
                    //Console.orig.log('complete');
                    await this.yun(); //firefox第一次在这里触发
                    //initApp();
                }
            });
            if(document.readyState == 'complete'){ //如果页面已经加载完成 (正常情况下不会触发)
                await this.yun();
            }
            window.addEventListener("beforeunload", async (event) => { //在页面关闭或刷新之前
                //保存设置
                // await DBConfig.SaveConfig("globalSettings", globalSettings); //保存配置到数据库
                //判断是否需要提示确认退出
                if(globalStatus.globalExitConfirm) {
                    event.preventDefault();
                    event.returnValue = "";
                }
            });
            return true;
        }
        async yun(){
            if(this.isRun) //只能运行一次
                return;
            this.isRun = true;
            let isExclude = false, isUnavailable = false;
            for (const [index, item] of App.URLRuleSet.entries()) {
                let currentURL = window.location.origin + window.location.pathname;
                if(URL.test(currentURL, item.urlType, item.url)){ //测试URL是否匹配
                    if(item.exclude.length > 0){
                        for (const [i, excludeItem] of item.exclude.entries()) {
                            if(URL.test(currentURL, item.urlType, excludeItem)){ //测试URL是否被排除
                                isExclude = true;
                                break;
                            }
                        }
                    }
                    if(item.usabilityTest.length > 0){
                        for (const [i, testItem] of item.usabilityTest.entries()) { //测试可用性
                            if(eval(testItem)){
                                isUnavailable = true;
                                break;
                            }
                        }
                    }

                    if(!isExclude && !isUnavailable){
                        if(await item.init.apply())
                            await item.entry.apply();
                    }
                    return;
                }
            }
        }
    }

    class UI {
        static loadingAnimation = {
            isShowLoadingAnimation: true,
            loadIndex: 0,
        }
        static loadingProgress = {
            isShowLoadingProgress: true,
            value: 0,
        }
        static init(){

            if(globalSettings.AppSettings.loading.isShowLoadingProgress){
                //加载模板
                Utils.addElementsByHTMLTemplateText(loadingUITemplate);
                //渲染进度条组件
                layui.element.render('progress', 'loading-filter-progress');
            }

            if(globalSettings.AppSettings.loading.isShowLoadingAnimation){
                UI.setShowLoadingAnimation(true);
            }
        }

        static setShowLoadingAnimation(isShow) {
            UI.loadingAnimation.isShowLoadingAnimation = isShow;

            layui.use(function(){
                let layer = layui.layer;
                
                if(isShow){
                    UI.loadingAnimation.loadIndex = layer.load(2); //显示加载动画
                }else{
                    layer.close(UI.loadingAnimation.loadIndex); //关闭加载动画
                }
            });
        }

        static setShowLoadingProgress(isShow) {
            UI.loadingProgress.isShowLoadingProgress = isShow;
            let loadingProgress = document.getElementById("loadingProgress");

            if(isShow){
                loadingProgress.style.display = "block";
            }else{
                setTimeout(()=>{
                    loadingProgress.style.display = "none";
                }, 500);
            }
        }

        static setValueByLoadingProgress(value) {
            if(value>100)
                value=100;
            else if(value<0)
                value=0;

            UI.loadingProgress.value = value;

            layui.element.progress('loading-filter-progress', `${value}%`); // 设置进度值
        }

        static addValueByLoadingProgress(value) {
            if(value>100)
                value=100;
            else if(value<0)
                value=0;

            layui.element.progress('loading-filter-progress', (UI.loadingProgress.value + value) + '%'); // 设置进度值
        }
    }

    // ---------------------------- 功能定义 ----------------------------
    class MortalBase {
        constructor(){
        }
        static init(){
            return true;
        }
        static run(){

        }
        static stop(){

        }
        static clean(){

        }
    }

    const OUTSTYLE = {__proto__: null,
        A: 1,
        B: 1 << 1,
        C: 1 << 2,
    }

    let strArray2 = []; //恶手选择器
    let strArray3 = []; //不一致选择器

    class PageExtend extends MortalBase {
        static perfor = new Performance("PageExtend");
        //
        static badChooseNum = 0;
        static badChooseNumCustom = 0;
        constructor(){
            //super();
        }
        static init(){
            //Console.orig.log("PageExtend.init()");
            if(Debug.getDebug){
                CodeTemplate.autoWired_Performance("PageExtend.run", PageExtend);
            }
            return true;
        }
        static async run(){
            //Console.orig.log("PageExtend.run()");
            const [result1, result2, result3] = 
                               //显示恶手 (收集数据)         //起始信息详细化 (独立) (重要)  //列出选择权重 (独立)
            (await Promise.all([PageExtend.showBadChoose(), PageExtend.showStartInfo(), PageExtend.showChooseWeight()]
            .map(item => item.catch(err => Debug.globalErrorHandle(err))) //每个并发请求使用catch进行对应异常的处理,防止某个请求失败后,影响到其他函数执行
            ));
            //Console.orig.log(result1, result2, result3);
            //修改 元数据 选项卡 (数据统计需要在showBadChoose()之后执行)
            await PageExtend.alterMetaData().catch(err => Debug.globalErrorHandle(err));
        }
        static stop(){

        }
        static clean(){

        }
        //显示恶手
        static async showBadChoose() {
            //Console.orig.log("MainApp.showBadChoose()");
            const getData = (strArray3, collapseEntry, kyokuTitleEle, summary, badChooseType) => {
                //收集数据2-不一致
                let nameID = "discord-" + strArray3.length;
                collapseEntry.setAttribute("name", nameID + "-main"); //设置子项name
                strArray3.push({
                    name: kyokuTitleEle.textContent + " " + summary.textContent,
                    parentHref: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
                    badChooseType: badChooseType,
                    //name: nameID + "-main", //name
                    type: "name",
                    nameID: nameID
                });
            };

            const orderLossEleArray = document.getElementsByClassName("order-loss");

            for (let i = 0; i < orderLossEleArray.length; i++) {
                const orderLoss = orderLossEleArray[i];

                const nChooseIndex = parseInt(orderLoss.innerText.match(/[\d]+/)[0]);
                const nChooseSum = parseInt(orderLoss.nextSibling.textContent.match(/[\d]+/)[0]);

                const turnInfo = orderLoss.parentElement;
                const summary = turnInfo.parentElement;
                const collapseEntry = summary.parentElement;

                const table = collapseEntry.lastChild.firstChild;
                const tbody = table.lastChild;

                const nChooseTR = tbody.childNodes[nChooseIndex -1];
                const nChooseWeightTD = nChooseTR.lastChild;
                const chosenWeight = parseFloat(nChooseWeightTD.innerHTML.replace(/<.*?>/g, "")); //过滤html标签, 只保留文字内容

                const kyokuTitleEle = collapseEntry.parentElement.parentElement.firstChild.getElementsByTagName("a")[0];

                if (chosenWeight <= parseFloat(globalSettings.Config.badMoveUpperLimit)) { //严重恶手
                    const badChooseNode = document.createElement("span");
                    badChooseNode.classList.add("badChoose");
                    badChooseNode.classList.add("level1");
                    badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveUp}`;
                    turnInfo.appendChild(badChooseNode);

                    collapseEntry.classList.add("collapseEntryL1");

                    PageExtend.badChooseNum++;

                    //收集数据3-恶手
                    let nameID = "badChoose-1-" + strArray2.length;
                    collapseEntry.id = nameID + "-main"; //设置子项id
                    strArray2.push({
                        name: kyokuTitleEle.textContent + " " + summary.textContent,
                        parentHref: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
                        badChooseType: 1,
                        //id: nameID + "-main", //id
                        type: "id",
                        nameID: nameID
                    });
                    getData(strArray3, collapseEntry, kyokuTitleEle, summary, 1);
                }else if (chosenWeight <= parseFloat(globalSettings.Config.badMoveUpperLimitCustom)) { //普通恶手
                    const badChooseNode = document.createElement("span");
                    badChooseNode.classList.add("badChoose");
                    badChooseNode.classList.add("level2");
                    badChooseNode.innerHTML = ` \u00A0\u00A0\u00A0${i18nText.badMove}${i18nText.badMoveDown}`;
                    turnInfo.appendChild(badChooseNode);

                    collapseEntry.classList.add("collapseEntryL2");

                    PageExtend.badChooseNumCustom++;

                    //收集数据3-恶手
                    let nameID = "badChoose-2-" + strArray2.length;
                    collapseEntry.id = nameID + "-main"; //设置子项id
                    strArray2.push({
                        name: kyokuTitleEle.textContent + " " + summary.textContent,
                        parentHref: kyokuTitleEle.href.match(/#[\S\s]+$/)[0],
                        badChooseType: 2,
                        //id: nameID + "-main", //id
                        type: "id",
                        nameID: nameID
                    });
                    getData(strArray3, collapseEntry, kyokuTitleEle, summary, 2);
                }else{
                    getData(strArray3, collapseEntry, kyokuTitleEle, summary);
                }

            } //for
            //Console.orig.log("end MainApp.showBadChoose()");
            UI.addValueByLoadingProgress(20);
        }
        //修改 元数据 选项卡
        static async alterMetaData() {
            //Console.orig.log("MainApp.alterMetaData()");
            let mortalMap = null;
            let mortal_New = null;
            let allMap = null;
            try {
                // 获取不同语言的 Mortal 版本
                let allJsonStr = await localforage.getItem("Mortal_All");
                if(allJsonStr != null) {
                    allMap = new Map(Object.entries(JSON.parse(allJsonStr)));
                }
                // 新增 显示 Mortal 版本
                const jsonStr = await localforage.getItem("Mortal_Type");
                mortal_New = await localforage.getItem("Mortal_New");
                if(jsonStr != null) {
                    let obj = Object.entries(JSON.parse(jsonStr));
                    mortalMap = new Map(obj);
                }
            } catch (err) {
                Console.orig.error("获取数据时发生错误: \n" + err);
            }

            // 修改 元数据 选项卡
            let metaData = null;
            const detailsElements = document.getElementsByTagName("details");
            for (let i = 0; i < detailsElements.length; i++) {
                const details = detailsElements[i];
                const summary = details.firstChild;
                if (summary.firstChild.textContent == i18nText.metaData) {
                    metaData = details;
                    metaData.toggleAttribute("open", true); //打开 元数据 选项卡
                    break;
                }
            }
            const metaDataDL = metaData.lastChild;
            let matchRatioDD = null;
            let version = null;
            for (let i = 0; i < metaDataDL.childNodes.length; i++) {
                const metaDataChild = metaDataDL.childNodes[i];
                if(metaDataChild.nodeName == "DT" && metaDataChild.textContent == i18nText.modelTag) {
                    let ele = metaDataDL.childNodes[i + 1];
                    
                    //判断当前是否是最新版本的mortal
                    if(mortalMap != null) {
                        let mortalValue = mortalMap.get(ele.innerText);
                        if(mortalValue != undefined) {
                            let currVer = mortalValue.match('(?<=").*?(?=")')[0]; //匹配引号里的内容
                            if(CustomUtils.compareVersion(currVer, "3.1") == -1) {
                                let aiEle = metaDataDL.childNodes[i - 1];
                                aiEle.classList.add("color1");
                                aiEle.innerText = aiEle.innerText + ` \u00A0\u00A0\u00A0${i18nText.badMoveError}`;
                            }
                        }
                    }
                    //处理当前版本
                    const oldHandle = ()=>{ //设置为上次访问主页记录的数据
                        if(mortalMap != null) {
                            let mortalValue = mortalMap.get(ele.innerText);
                            if(mortalValue != undefined) {
                                ele.innerText = mortalValue;
                            }
                        }
                    }
                    //设置为之前访问主页记录的多语言数据
                    const pageLanguage = g_origLang;
                    if(allMap != null) {
                        let targetMortalMap = allMap.get(pageLanguage);
                        if(targetMortalMap != undefined){
                            let mortalValue = targetMortalMap.get(ele.innerText);
                            if(mortalValue != undefined) {
                                ele.innerText = mortalValue;
                            }
                        }else{ //目标数据中没有目标语言
                            oldHandle();
                        }
                    }else{ //没有目标数据
                        oldHandle();
                    }
                    
                }
                if (metaDataChild.nodeName == "DT" && metaDataChild.textContent == i18nText.matchRatio) {
                    matchRatioDD = metaDataDL.childNodes[i + 1];
                    version = metaDataDL.childNodes[i + 2];
                    //
                    metaDataDL.childNodes[i-2].classList.add("color2"); //rating
                    metaDataDL.childNodes[i-1].classList.add("color2");
                    metaDataDL.childNodes[i].classList.add("color2"); //AI 一致率
                    metaDataDL.childNodes[i+1].classList.add("color2");
                    break;
                }
            }
            const matchRatioText = matchRatioDD.textContent;
            const chooseNumStr = matchRatioText.substring(matchRatioText.indexOf("/") + 1);
            const chooseNum = parseInt(chooseNumStr);

            const badChooseRatioDT = document.createElement("dt");
            badChooseRatioDT.classList.add("color3");
            badChooseRatioDT.innerHTML = `${i18nText.badMoveRatio} ${globalSettings.Config.badMoveUpperLimit}%`;
            
            const badChooseRatioDD = document.createElement("dd");
            badChooseRatioDD.classList.add("color3");
            badChooseRatioDD.innerHTML = `${PageExtend.badChooseNum}/${chooseNum} = ${(100 * PageExtend.badChooseNum / chooseNum).toFixed(3)}%`;
            metaDataDL.insertBefore(badChooseRatioDD, version);
            metaDataDL.insertBefore(badChooseRatioDT, badChooseRatioDD);

            /* 新增 计算总恶手数 */
            PageExtend.badChooseNumCustom += PageExtend.badChooseNum; //计算总恶手数

            const badChooseRatioDT2 = document.createElement("dt");
            badChooseRatioDT2.classList.add("color3");
            badChooseRatioDT2.innerText = `${i18nText.badMoveRatio} ${globalSettings.Config.badMoveUpperLimitCustom}%`;
            
            const badChooseRatioDD2 = document.createElement("dd");
            badChooseRatioDD2.classList.add("color3");
            badChooseRatioDD2.innerHTML = `${PageExtend.badChooseNumCustom}/${chooseNum} = ${(100 * PageExtend.badChooseNumCustom / chooseNum).toFixed(3)}%`;
            
            metaDataDL.insertBefore(badChooseRatioDD2, version);
            metaDataDL.insertBefore(badChooseRatioDT2, badChooseRatioDD2);
            //Console.orig.log("end MainApp.alterMetaData()");
            UI.addValueByLoadingProgress(10);
        }
        //起始信息详细化
        static async showStartInfo() {
            //Console.orig.log("MainApp.showStartInfo()");
            /* 起始信息详细化 */
            function parmeHandle(eastScoreChange, southScoreChange, westScoreChange, northScoreChange) {
                let scoreArray = [{sc: eastScoreChange, i: 0}, {sc: southScoreChange, i: 1}, {sc: westScoreChange, i: 2}, {sc: northScoreChange, i: 3}];
                let newScoreArray = scoreArray.filter((obj) => {
                    return obj.sc != 0;
                });
                let scoreAddArray = newScoreArray.filter((obj) => { //荣和的玩家
                    return obj.sc > 0;
                });
                let scoreSubArray = newScoreArray.filter((obj) => { //放铳的玩家
                    return obj.sc < 0;
                });
                scoreAddArray.sort((a,b)=>{return b.sc-a.sc});

                return {scoreArray: scoreArray, newScoreArray: newScoreArray, scoreAddArray: scoreAddArray, scoreSubArray: scoreSubArray};
            }

            function handleRon(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理荣和
                let obj = parmeHandle(eastScoreChange, southScoreChange, westScoreChange, northScoreChange);
                let scoreAddArray = obj.scoreAddArray;
                let scoreSubArray = obj.scoreSubArray;
                let selfPlayerIndex = MJCommonUtils.getPlayerIndexByPlayerSeatName(MJCommonUtils.getPlayerSeatNameByPlayerIndex(startPlayerIndex, kyoku, OUTSTYLE.B), OUTSTYLE.B);
                //
                let str = "";
                for (let i = 0; i < scoreAddArray.length; i++) {
                    const scoreAdd = scoreAddArray[i];

                    let scAddPlayerSeatName = MJCommonUtils.getPlayerSeatNameByPlayerIndex(scoreAdd.i, kyoku, OUTSTYLE.B); //荣和的玩家
                    let scAddPlayerViewName = MJCommonUtils.getSelfViewPlayerNameByTargetPlayerIndex(selfPlayerIndex, MJCommonUtils.getPlayerIndexByPlayerSeatName(scAddPlayerSeatName, OUTSTYLE.B));

                    let scSubPlayerSeatName = MJCommonUtils.getPlayerSeatNameByPlayerIndex(scoreSubArray[0].i, kyoku, OUTSTYLE.B); //放铳的玩家
                    let scSubPlayerViewName = MJCommonUtils.getSelfViewPlayerNameByTargetPlayerIndex(selfPlayerIndex, MJCommonUtils.getPlayerIndexByPlayerSeatName(scSubPlayerSeatName, OUTSTYLE.B));

                    str += `${scAddPlayerSeatName} (${scAddPlayerViewName}) ` + 
                    `<span class="color4">${i18nText.Ron}</span> ${scSubPlayerSeatName} (${scSubPlayerViewName}) +${scoreAdd.sc} ${scoreSubArray[0].sc}`;
                }

                return str;
            }
            function handleTsumo(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理自摸
                
            }
            function handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange, southScoreChange, westScoreChange, northScoreChange) { //处理流局
                
            }
            
            const summaryEle = document.getElementsByClassName("kyoku-toc")[0];
            if(summaryEle == undefined)
                return;
            summaryEle.classList.add("position_re");
            for (let j = 0; j < summaryEle.children.length; j++) {
                const summary = summaryEle.children[j];
                summary.classList.add("min-w-20"); //
            }
            summaryEle.children[1].classList.add("position_ab");
            summaryEle.children[1].classList.add("l-196px"); //
            const kyokuEle = summaryEle.getElementsByTagName("a");
            const endInfoEle = summaryEle.getElementsByClassName("end-status");
            //
            const section = document.getElementsByTagName("section");
            for (let i = 0, length = section.length; i != length; ++i) {
                const titleEle = section[i].children[0];
                const titleKyokuEle = titleEle.getElementsByTagName("a"); //只有1个元素
                const titleEndInfoEle = titleEle.getElementsByClassName("end-status"); //只有1个元素

                const tenhouData = section[i].getElementsByTagName("iframe")[0].src;
                const playerIndexStr = tenhouData.match(/tw=[0-3]/)[0];
                const startPlayerIndex = parseInt(playerIndexStr.substring(playerIndexStr.length -1)); //起始玩家索引
                const json = JSON.parse(decodeURI(tenhouData.substring(tenhouData.indexOf("{")))); //天凤对局数据
                const kyoku = json.log[0][0][0]; //局数
                const count = json.log[0][0][1]; //本场数
                const currScore = json.log[0][1]; //当前点数
                const scoreChange = json.log[0][json.log[0].length -1]; //点数变动
                const endMode = scoreChange[0];

                //解析规则
                if(matchRule.isInit == false) {
                    const disp = json.rule.disp;

                    if(disp.indexOf("間") != -1) { //雀魂
                        matchRule.isRon3 = true;
                        //Console.orig.log("雀魂牌谱");
                    }else if(disp.indexOf("Player") != -1) { //一番街
                        matchRule.isRon3 = false;
                        //Console.orig.log("一番街牌谱");
                    }else{ //默认为天凤
                        matchRule.isRon3 = false;
                        //Console.orig.log("默认为天凤牌谱(包括自定义牌谱)");
                    }
                    matchRule.isInit = true;
                }

                //四家当前分数
                const eastScore = currScore[0]; //东
                const southScore = currScore[1]; //南
                const westScore = currScore[2]; //西
                const northScore = currScore[3]; //北
                //四家分数变化(直接) //送棒的-1000没有显示
                let eastScoreChange = [];
                let southScoreChange = [];
                let westScoreChange = [];
                let northScoreChange = [];

                let ronCount = (scoreChange.length -1) / 2;
                if(scoreChange.length > 1) { //比如九种九牌, 是没有分数变化的数据的
                    //是否有多家荣和
                    for (let j = 0; j < ronCount; j++) { //处理可能的多家荣和
                        eastScoreChange.push(scoreChange[1+ j*2][0]); //东
                        southScoreChange.push(scoreChange[1+ j*2][1]); //南
                        westScoreChange.push(scoreChange[1+ j*2][2]); //西
                        northScoreChange.push(scoreChange[1+ j*2][3]); //北
                    }
                }
                //判断模式
                if(endMode == tenhouText.Ron) { //自摸、荣和
                    let str = "";
                    for (let j = 0; j < ronCount; j++) { //处理可能的多家荣和
                        if(j>0) //处理多家
                            str += ", ";
                        
                        let obj = parmeHandle(eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                        
                        if(eastScoreChange[j] == 0 || 
                            southScoreChange[j] == 0 || 
                            westScoreChange[j] == 0 || 
                            northScoreChange[j] == 0) { //如果有任何一家分数变动为0, 则为荣和
                            str += handleRon(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                        }if(obj.scoreAddArray.length == 3) { //判断是否是三家荣和
                            if(matchRule.isRon3) //如果启用了三家和了的规则 //? 可能是没有必要的判断? 等待使用三种游戏牌谱分别进行查证
                                str += handleRon(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                            else{ //流局 //? 可能是没有必要的判断? 等待使用三种游戏牌谱分别进行查证
                                // let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                                // Console.orig.log(str);
                            }

                        }else{ //否则都是自摸
                            // str += handleTsumo(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                        }
                    }//for
                    if(str.length > 0) {
                        const span = document.createElement("span");
                        span.innerHTML = ` \u00A0\u00A0\u00A0` + str;
                        endInfoEle[i].parentElement.appendChild(span);

                        let lineNum = CustomUtils.getTextLineNum(span.parentNode);
                        if(lineNum > 1){ //如果新添加的文字有多行, 则进行对齐
                            for (let j = 1; j < lineNum; j++) {
                                summaryEle.children[0].insertBefore(document.createElement("br"), kyokuEle[i].parentElement.nextElementSibling);
                            }
                        }
                    }
                }else if(endMode == tenhouText.Ryuukyoku){ //荒牌流局 //流局, 如果有分数改变则处理
                    // let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                    // Console.orig.log(str);
                }else if(endMode == tenhouText.RyuukyokuTsumo) { //流局满贯 (等同于自摸8000)
                    // let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                    // Console.orig.log(str);
                }else if(endMode == tenhouText.RyuukyokuType1) { //九种九牌 //流局, 如果有分数改变则处理
                    // let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                    // Console.orig.log(str);
                }else{ //四风连打、四杠散了、四家立直 //三家和了 //流局, 如果有分数改变则处理
                    // let str = handleRyuukyoku(kyoku, startPlayerIndex, eastScoreChange[j], southScoreChange[j], westScoreChange[j], northScoreChange[j]);
                    // Console.orig.log(str);
                }

                const span = document.createElement("span");
                let str = MJCommonUtils.getPlayerSeatNameByPlayerIndex(startPlayerIndex, kyoku, OUTSTYLE.A);
                span.innerText = ` \u00A0\u00A0\u00A0` + str;
                span.classList.add("position_ab");
                span.classList.add("l-130px");
                if(str == i18nText.seatTypeA0) //东起
                    span.classList.add("color5");
                else
                    span.classList.add("color6");
                kyokuEle[i].parentElement.appendChild(span);
                titleKyokuEle[0].innerText += `\u00A0\u00A0` + str;

                titleKyokuEle[0].id = titleKyokuEle[0].href.match(/#[\S\s]+$/)[0] + "-main"; //设置父项id
            }//for
            //Console.orig.log("end MainApp.showStartInfo()");
            UI.addValueByLoadingProgress(20);
        }
        //列出选择权重
        static async showChooseWeight() {
            //Console.orig.log("MainApp.showChooseWeight()");
            /* 列出选择权重 */
            const defaultHandleFunc = (newNode, differData, index, colorStr) => {
                newNode.style.color = colorStr; //设置为目标颜色
                newNode.innerHTML = ` \u00A0\u00A0\u00A0` + eval("i18nText.badMoveDiffer" + index) + differData;
            }
            const map = new Map(); //使用map保证重置循坏后的唯一性
            
            let targClassName;
            if(g_origLang == "en"){
                targClassName = "l-210px";
            }else{
                targClassName = "l-170px";
            }

            const boxObj = {__proto__: null,
                left:0,
                top:0
            };
            const entry = document.getElementsByClassName("collapse entry");
            let spanSelf, spanMortal;
            for (let i = 0, length = entry.length; i != length; ++i) {
                entry[i].classList.add("position_re");

                const roleEle = entry[i].getElementsByClassName("role");

                let selfPai = roleEle[0].parentElement;
                let mortalPai = roleEle[1].nextElementSibling;

                if(mortalPai.tagName.toLocaleLowerCase() == 'details') {
                    mortalPai = roleEle[1].nextSibling;
                }

                if (DOMTypeUtils.isSVGElement(selfPai.childNodes[selfPai.childNodes.length -2])) {
                    if(selfPai.childNodes[selfPai.childNodes.length -2].tagName.toLocaleLowerCase() == 'svg') { //如果有多张牌图片,就使用最后一张牌图片
                        selfPai = selfPai.childNodes[selfPai.childNodes.length -2];
                    }
                }


                if(mortalPai.nextElementSibling.tagName.toLocaleLowerCase() == 'svg') { //如果有多张牌图片,就使用最后一张牌图片
                    mortalPai = mortalPai.nextElementSibling;
                }

                const dataEle = entry[i].getElementsByTagName("tbody")[0].childNodes;

                let selfPaiData = 0;
                let mortalPaiData = 0;
                let selfBoxObj = { ...boxObj }, mortalBoxObj = { ...boxObj }; //复制对象
                map.clear(); //清除map

                let j = 0, size = dataEle.length;
                while (j != size) {
                    let selfPaiStr = null;
                    let mortalPaiStr = null;

                    let isResetLoop = false; //是否重置循坏

                    if(selfPai != null){
                        if(DOMTypeUtils.isTextElement(selfPai)) {
                            selfPaiStr = selfPai.data;
                        }else{
                            let obj = selfPai.getElementsByClassName("face");
                            if(obj[0] != null){
                                selfPaiStr = obj[0].href.baseVal;
                            }else{ //选择跳过的情况
                                selfPaiStr = selfPai.childNodes[selfPai.childNodes.length -1].data;
                            }
                        }
                    }
                    if(mortalPai != null){
                        if(DOMTypeUtils.isTextElement(mortalPai)) {
                            mortalPaiStr = mortalPai.data;
                        }else{
                            let obj = mortalPai.getElementsByClassName("face");
                            if(obj[0] != null){
                                mortalPaiStr = obj[0].href.baseVal;
                            }else{ //选择跳过的情况
                                mortalPaiStr = mortalPai.childNodes[mortalPai.childNodes.length -1].data;
                            }
                        }
                    }

                    let data = dataEle[j].childNodes[2].innerHTML.replace(/<.*?>/g, ""); //过滤html标签, 只保留文字内容

                    let obj1 = dataEle[j].childNodes[0].getElementsByClassName("face");
                    let dataPaiStr;
                    if(obj1[obj1.length - 1] != null) { // 如果有多张牌, 则选择最后一张牌作为对比牌 (主要用于吃的情况、碰杠这些牌都是一样的)
                        dataPaiStr = obj1[obj1.length - 1].href.baseVal;
                    }else{
                        dataPaiStr = dataEle[j].childNodes[0].innerHTML;

                        if(map.has(j) == false) {
                            map.set(j, true); //保存当前j的值,防止重复开始循坏
                            j = 0; //如果 有 选择跳过的情况, 则重新开始循坏, 以找到正确的数据
                            isResetLoop = true;
                        }
                    }

                    if(selfPaiStr == dataPaiStr) { //如果目标操作是自己的操作

                        selfPaiData = data;

                        spanSelf = document.createElement("span");
                        spanSelf.innerText = ` \u00A0\u00A0\u00A0` + data;
                        spanSelf.classList.add("position_ab");

                        if(selfBoxObj.top == 0) {
                            if(!isNaN(selfPai.offsetTop)) {
                                selfBoxObj.top = (selfPai.offsetTop + selfPai.offsetHeight / 2 - 10);
                            }else if(!isNaN(selfPai.parentElement.offsetTop)) {
                                selfBoxObj.top = (selfPai.parentElement.offsetTop + selfPai.parentElement.offsetHeight / 2 - 10);
                            }else{
                                Console.orig.error("[dom struct inconsistency] source:", "selfPai");
                                Console.orig.log("[debug]", `i: ${i}`);
                            }
                            spanSelf.style.top = selfBoxObj.top + 2 + "px";
                        }
                        spanSelf.classList.add(targClassName); //l-170px //l-210px

                        entry[i].insertBefore(spanSelf, entry[i].childNodes[3].nextSibling);

                        selfPai = null; //置null, 防止继续计算
                    }else if(mortalPaiStr == dataPaiStr) { //如果目标操作是Mortal的操作

                        mortalPaiData = data;

                        spanMortal = document.createElement("span");
                        spanMortal.innerText = ` \u00A0\u00A0\u00A0` + data;
                        spanMortal.classList.add("position_ab");

                        if(mortalBoxObj.top == 0) {
                            if(!isNaN(mortalPai.offsetTop)) {
                                mortalBoxObj.top = (mortalPai.offsetTop + mortalPai.offsetHeight / 2 - 10);
                            }else if(!isNaN(mortalPai.previousElementSibling.offsetTop)) {
                                mortalBoxObj.top = (mortalPai.previousElementSibling.offsetTop + mortalPai.previousElementSibling.offsetHeight / 2 - 10);
                            }else if(!isNaN(mortalPai.previousElementSibling.previousElementSibling.offsetTop)) {
                                mortalBoxObj.top = (mortalPai.previousElementSibling.previousElementSibling.offsetTop + mortalPai.previousElementSibling.previousElementSibling.offsetHeight / 2 - 10);
                            }else{
                                Console.orig.error("[dom struct inconsistency] source:", "mortalPai");
                                Console.orig.log("[debug]", `i: ${i}`);
                            }
                            spanMortal.style.top = mortalBoxObj.top + 1 + "px";
                        }
                        spanMortal.classList.add(targClassName); //l-170px //l-210px

                        if(DOMTypeUtils.isTextElement(mortalPai.nextSibling)) { //如果有多张牌图片,就使用最后一张牌图片后面的文字的位置
                            mortalPai = mortalPai.nextSibling;
                        }

                        entry[i].insertBefore(spanMortal, mortalPai.nextSibling);

                        mortalPai = null; //置null, 防止继续计算
                    }

                    if(selfPaiStr == mortalPaiStr) { //如果自己选择打出的牌与mortal选择打出的牌相同
                        if(map.has(j) == false) {
                            map.set(j, true); //保存当前j的值,防止重复开始循坏
                            j = 0; //如果 有 选择跳过的情况, 则重新开始循坏, 以找到正确的数据
                            isResetLoop = true;
                        }
                    }

                    if(selfPai == null && mortalPai == null) { //是否处理完毕
                        break; //跳出循坏
                    }
                    if(isResetLoop == false){ //不重置循坏时, index++
                        ++j;
                    }
                }//for

                /* 计算自己的选择与mortal选择的差值 */
                if(mortalPaiData == selfPaiData) //忽略自己和mortal打出的牌一样的结果
                    continue;
                const differData = Math.constructor.roundEx(Math.abs(mortalPaiData - selfPaiData), 5); //保留5位小数

                const turnInfo = entry[i].children[0].children[0];
                const newNode = document.createElement("span");
                newNode.classList.add("font_weight_400");

                if (differData < 5) { //微差
                    defaultHandleFunc(newNode, differData, 1, "#000"); //黑色
                }else if (differData < 10) { //小幅差距
                    defaultHandleFunc(newNode, differData, 2, "#996633"); //褐色
                }else if (differData < 20) { //低等差距
                    defaultHandleFunc(newNode, differData, 3, "#009966"); //淡绿
                }else if (differData < 40) { //中等差距
                    defaultHandleFunc(newNode, differData, 4, "#3399FF"); //淡蓝
                }else if (differData < 60) { //高等差距
                    defaultHandleFunc(newNode, differData, 5, "#3333CC"); //深蓝
                }else if (differData < 80) { //大幅度差距
                    defaultHandleFunc(newNode, differData, 6, "#CC0099"); //淡红
                }else{ //压倒性差距
                    defaultHandleFunc(newNode, differData, 7, "#f00"); //红色
                }
                turnInfo.appendChild(newNode);

                let lineNum = CustomUtils.getTextLineNum(turnInfo);
                let offsetTopDiffValue = Math.abs(roleEle[0].offsetTop - spanSelf.offsetTop);
                if(lineNum > 1 && offsetTopDiffValue >= 5){ //如果新添加的文字有多行, 则进行对齐 //移动设备x轴分辨率较小,并且各种设备分辨率不同,就会导致可能不需要对齐
                    let lineHeight = CustomUtils.getStyleOfLineHeight(turnInfo);
                    let origTopSelf = parseFloat(spanSelf.style.top);
                    let origTopMortal = parseFloat(spanMortal.style.top);
                    spanSelf.style.top = origTopSelf + lineHeight + "px";
                    spanMortal.style.top = origTopMortal + lineHeight + "px";
                }
            }//for
            //Console.orig.log("end MainApp.showChooseWeight()");
            UI.addValueByLoadingProgress(20);
        }
        
    }

    class MainApp extends MortalBase {
        static perfor = new Performance("MainApp");
        static badChooseMap = new Map(); //恶手选择器
        static disable_kyokuChoose = false;
        static disable_badChoose = false;
        static disable_diffChoose = false;
        constructor(){
            //super();
        }
        static init(){
            //Console.orig.log("MainApp.init()");
            if(Debug.getDebug){
                CodeTemplate.autoWired_Performance("MainApp.run", MainApp);
            }
            return true;
        }
        static async run(){
            //Console.orig.log("MainApp.run()");
            const [result1, result2] = 
                               //显示恶手 (收集数据)         //起始信息详细化 (独立) (重要)  //列出选择权重 (独立)
            (await Promise.all([MainApp.createCatalogUI(), MainApp.createSettingUI()]
            .map(item => item.catch(err => Debug.globalErrorHandle(err))) //每个并发请求使用catch进行对应异常的处理,防止某个请求失败后,影响到其他函数执行
            ));
            //Console.orig.log(result1, result2);
            await MainApp.end();
        }
        static async end() {
            //Console.orig.log("MainApp.end()");

            /* 将页面滚动导致的元素可见性改变与选择器绑定 (全局滚动) */
            let section = document.getElementsByTagName("section");
            let curKyoku; //局数
            let curCount; //本场数
            const globalScrollHandle = (e) => {
                const scrollTop = document.scrollingElement.scrollTop;
                const heightOffset = document.scrollingElement.clientHeight / 2;
                for (let i = 0; i < section.length; i++) {
                    if ((section.length == 1) || //比如自定义牌谱,只有1局,就跳过后续的可见性测试
                        (i == 0 && scrollTop +heightOffset < section[1].offsetTop) || //第一个节点高度范围 //0~第二个元素之间的高度 //小于第二个元素的高度
                        (i == section.length -1 && scrollTop >= section[section.length -2].offsetTop) || //最后一个节点高度范围 //大于倒数第二个元素的高度
                        (scrollTop +heightOffset >= section[i].offsetTop && scrollTop +heightOffset < section[i+1].offsetTop)) { //中间的节点高度范围 //相邻的两个节点之间
                            const titleEle = section[i].children[0];
                            const titleKyokuEle = titleEle.getElementsByTagName("a"); //只有1个元素
                            //Console.orig.log("可见: " + titleKyokuEle[0].innerText, titleKyokuEle[0].id);
                            if(!MainApp.disable_kyokuChoose){
                                let targetEle = document.getElementById(titleKyokuEle[0].id.replace("-main", ""));
                                DomUtils.setNewActivateEle(targetEle, "layui-this");
                            }
                            [curKyoku, curCount] = titleKyokuEle[0].id.split("-").filter((item) => {
                                return item.match(/\d+/); //过滤非数字
                            });
                            
                            let curkyokuMode = MJCommonUtils.normalKyokuToKyokuMode(curKyoku); //标准局数 转 东南西场 (1=东 2=南 3=西)

                            let textKyoku = MJCommonUtils.normalKyokuToTextKyoku(curKyoku, curkyokuMode); //标准局数 转 文本局数 (0=>1: 东一) (6=>3: 南3)

                            //恶手选择器
                            if(!MainApp.disable_badChoose){
                                let badChooseArray = MainApp.badChooseMap.get(`${curkyokuMode}${textKyoku}${curCount}`);
                                const handleBadChoose = (badChooseArray)=>{
                                    if(badChooseArray != undefined){
                                        for (let j = 0; j < badChooseArray.length; j++) {
                                            const badChooseEle = document.getElementById(badChooseArray[j]);
                                            
                                            if(j==0){ //第一个元素
                                                DomUtils.setNewActivateEle(badChooseEle, "layui-this");
                                                // if(badChooseArray.length == 1){ //只有1个元素
                                                //     badChooseEle.classList.add("badChoose_border");
                                                // }
                                                // else{ //多个元素
                                                //     badChooseEle.classList.add("badChoose_border_first");
                                                // }
                                            }else if(j == badChooseArray.length -1){ //最后一个
                                                // badChooseEle.classList.add("badChoose_border_last");
                                            }else{ //中间的
                                                // badChooseEle.classList.add("badChoose_border_middle");
                                            }
                                        }
                                    }else{
                                        Console.orig.error("badChooseArray is undefined");
                                    }
                                };

                                if(badChooseArray == undefined){ //如果当前的对局,比如东一,没有恶手,那么就是undefined //则继续向下查找有恶手的对局
                                    for (let index = i + 1; index < section.length; index++) {
                                        const titleEle = section[index].children[0];
                                        const titleKyokuEle = titleEle.getElementsByTagName("a"); //只有1个元素

                                        let targetEle = document.getElementById(titleKyokuEle[0].id.replace("-main", ""));
                                        [curKyoku, curCount] = titleKyokuEle[0].id.split("-").filter((item) => {
                                            return item.match(/\d+/); //过滤非数字
                                        });

                                        let curkyokuMode = MJCommonUtils.normalKyokuToKyokuMode(curKyoku); //标准局数 转 东南西场 (1=东 2=南 3=西)

                                        let textKyoku = MJCommonUtils.normalKyokuToTextKyoku(curKyoku, curkyokuMode); //标准局数 转 文本局数 (0=>1: 东一) (6=>3: 南3)

                                        badChooseArray = MainApp.badChooseMap.get(`${curkyokuMode}${textKyoku}${curCount}`); //循坏获取,直到获取到有恶手的对局
                                        if(badChooseArray != undefined){ //找到了就跳出
                                            break;
                                        }
                                    }
                                }
                                if(badChooseArray != undefined){
                                    // Console.orig.log("当前局数没有恶手,向后查找并设置!");
                                    handleBadChoose(badChooseArray);
                                }else{
                                    // Console.orig.log("当前查看的对局没有恶手!");
                                    let ele = document.querySelector(".ui #selector2");
                                    if(ele.children.length == 0){
                                        // Console.orig.log("当前牌谱没有恶手!");
                                    }else{
                                        // Console.orig.log("设置为第一个!");
                                        DomUtils.setNewActivateEle(ele.children[0].children[0], "layui-this"); //设置为第一个
                                    }
                                }
                            }
                            
                            //不一致选择器
                            if(!MainApp.disable_diffChoose){

                            }

                            MainApp.disable_kyokuChoose = false;
                            MainApp.disable_badChoose = false;
                            MainApp.disable_diffChoose = false;
                            return;
                    }//if
                }//for
            };
            window.addEventListener("scroll", CustomUtils.throttle(globalScrollHandle, 100));
            globalScrollHandle();
            
            /* 绑定按钮事件 */
            const Mode = {
                prev: "prev",
                next: "next",
            }
            const commonButClickEvent = (selectorStr, butName, mode)=>{
                let nextEle = null;
                let ele = document.querySelector(selectorStr);
                if(ele == undefined){
                    Console.orig.warn("click", butName, "获取到的目标节点为空!"); return;
                }
                
                if(mode == Mode.prev){
                    nextEle = ele.previousElementSibling;
                }else if(mode == Mode.next){
                    nextEle = ele.nextElementSibling;
                }
                if(nextEle != null){
                    nextEle = nextEle.children[0];
                    DomUtils.setNewActivateEle(nextEle, "layui-this");
                    nextEle.click();
                }
            };
            document.getElementById("but_kyoku_prev").addEventListener("click", function(e){
                let selectorName = ".ui #selector1 .layui-this";
                let ele = document.querySelector(selectorName);
                if(ele == undefined){
                    Console.orig.warn("click", "but_kyoku_prev", "获取到的目标节点为空!"); return;
                }
                
                ele = ele.children[0];
                
                let idStr = ele.id; //
                let targetNode = document.getElementById(idStr + "-main");
                if(targetNode == null){
                    Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                }
                
                let parentEle = targetNode.parentElement.parentElement.parentElement;
                let collapseEntry = parentEle.children[3].querySelectorAll(".collapse.entry");

                let liEle = ele.parentElement.parentElement.parentElement;
                if(!liEle.classList.contains("layui-nav-itemed")){ //自动打开选项卡
                    liEle.classList.add("layui-nav-itemed");
                }

                if(!CustomUtils.isShowInClientWindowOfPositionModeAndScrollbar(ele)){ //如果没有显示出来,则定位到目标元素
                    CustomUtils.setScrollToTargetNode(ele, "center");
                }

                if(!CustomUtils.isShowInClientWindowOfNodeArray(collapseEntry)){ //如果没有显示出来,则先定位到目标元素,再执行切换操作
                    let changeEle = parentEle.getElementsByClassName("sticky")[0];
                    changeEle.style.background = "transparent";
                    CustomUtils.setHighlightShow(parentEle, 750, ()=>{
                        changeEle.style.background = "";
                    }); //高亮显示
                    CustomUtils.setScrollToTargetNode(parentEle, "start");
                }else{
                    commonButClickEvent(selectorName, "#but_kyoku_prev", Mode.prev);
                }
            }, false);

            document.getElementById("but_kyoku_next").addEventListener("click", function(e){
                let selectorName = ".ui #selector1 .layui-this";
                let ele = document.querySelector(selectorName);
                if(ele == undefined){
                    Console.orig.warn("click", "but_kyoku_next", "获取到的目标节点为空!"); return;
                }
                ele = ele.children[0];
                
                let idStr = ele.id; //
                let targetNode = document.getElementById(idStr + "-main");
                if(targetNode == null){
                    Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                }
                let parentEle = targetNode.parentElement.parentElement.parentElement;
                let collapseEntry = parentEle.children[3].querySelectorAll(".collapse.entry");

                let liEle = ele.parentElement.parentElement.parentElement;
                if(!liEle.classList.contains("layui-nav-itemed")){ //自动打开选项卡
                    liEle.classList.add("layui-nav-itemed");
                }

                if(!CustomUtils.isShowInClientWindowOfPositionModeAndScrollbar(ele)){ //如果没有显示出来,则定位到目标元素
                    CustomUtils.setScrollToTargetNode(ele, "center");
                }

                if(!CustomUtils.isShowInClientWindowOfNodeArray(collapseEntry)){ //如果没有显示出来,则先定位到目标元素,再执行切换操作
                    let changeEle = parentEle.getElementsByClassName("sticky")[0];
                    changeEle.style.background = "transparent";
                    CustomUtils.setHighlightShow(parentEle, 750, ()=>{
                        changeEle.style.background = "";
                    }); //高亮显示
                    CustomUtils.setScrollToTargetNode(parentEle, "start");
                }else{
                    commonButClickEvent(selectorName, "#but_kyoku_next", Mode.next);
                }
            }, false);

            document.getElementById("but_diff_prev").addEventListener("click", function(e){
                let selectorName = ".ui #selector2 .layui-this";
                let ele = document.querySelector(selectorName);
                if(ele == undefined){
                    Console.orig.warn("click", "but_diff_prev", "获取到的目标节点为空!"); return;
                }
                ele = ele.children[0];
                
                let idStr = ele.id; //
                let targetNode = document.getElementById(idStr + "-main");
                if(targetNode == null){
                    Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                }

                let liEle = ele.parentElement.parentElement.parentElement;
                if(!liEle.classList.contains("layui-nav-itemed")){ //自动打开选项卡
                    liEle.classList.add("layui-nav-itemed");
                }

                if(!CustomUtils.isShowInClientWindowOfPositionModeAndScrollbar(ele)){ //如果没有显示出来,则定位到目标元素
                    CustomUtils.setScrollToTargetNode(ele, "center");
                }

                if(!CustomUtils.isShowInClientWindow(targetNode)){ //如果没有显示出来,则先定位到目标元素,再执行切换操作
                    CustomUtils.setHighlightShow(targetNode, 750); //高亮显示
                    CustomUtils.setScrollToTargetNode(targetNode, "center");
                }else{
                    commonButClickEvent(selectorName, "#but_diff_prev", Mode.prev);
                }
            }, false);

            document.getElementById("but_diff_next").addEventListener("click", function(e){
                let selectorName = ".ui #selector2 .layui-this";
                let ele = document.querySelector(selectorName);
                if(ele == undefined){
                    Console.orig.warn("click", "but_diff_next", "获取到的目标节点为空!"); return;
                }
                ele = ele.children[0];
                
                let idStr = ele.id; //
                let targetNode = document.getElementById(idStr + "-main");
                if(targetNode == null){
                    Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                }

                let liEle = ele.parentElement.parentElement.parentElement;
                if(!liEle.classList.contains("layui-nav-itemed")){ //自动打开选项卡
                    liEle.classList.add("layui-nav-itemed");
                }

                if(!CustomUtils.isShowInClientWindowOfPositionModeAndScrollbar(ele)){ //如果没有显示出来,则定位到目标元素
                    CustomUtils.setScrollToTargetNode(ele, "center");
                }

                if(!CustomUtils.isShowInClientWindow(targetNode)){ //如果没有显示出来,则先定位到目标元素,再执行切换操作
                    CustomUtils.setHighlightShow(targetNode, 750); //高亮显示
                    CustomUtils.setScrollToTargetNode(targetNode, "center");
                }else{
                    commonButClickEvent(selectorName, "#but_diff_next", Mode.next);
                }
            }, false);

            UI.addValueByLoadingProgress(10);

            //Console.orig.log("end MainApp.end()");
        }
        static stop(){

        }
        static clean(){

        }
        //
        static async createCatalogUI() {
            //Console.orig.log("MainApp.createCatalogUI()");
            const summaryEle = document.getElementsByClassName("kyoku-toc")[0];
            //数据处理
            let catalogUI = document.getElementById("catalogUI");
            DomUtils.setMoveable(catalogUI, true);

            layui.use(function(){
                let element = layui.element;
                let laytpl = layui.laytpl;
                let $ = layui.$;
                //收集数据1-对局
                let strArray1 = [];
                let array = summaryEle.children[0].children;
                let length = summaryEle.children[0].children.length;
                for (let j = 0; j < length; j++) {
                    const ele = array[j];
                    if(ele.children.length == 0)
                        continue; //过滤<br>等没有子节点的单标签
                    strArray1.push({
                        name: ele.children[0].innerText + (ele.children[1] != undefined ? (" " + ele.children[1].innerText.trim()) : ""),
                        type: "id",
                        nameID: ele.children[0].href.match(/#[\S\s]+$/)[0],
                    });
                }

                let data = { //模板数据
                    selector1: strArray1, //对局选择器
                    selector2: strArray2, //恶手选择器
                    selector3: strArray3, //不一致选择器
                };

                //使用模板进行解析
                let compile = laytpl(catalogUITemplate); // 模板解析
                compile.render(data, (htmlStr)=>{
                    $('#catalogUI').html(htmlStr);
                }); // 模板渲染
                
                // 渲染导航组件
                element.render('nav', 'selector-filter-nav');

                /* 新增右置对局选择器和切换器(固定/可滑动呼出) */
                let catalogUIBuf = document.getElementById("catalogUIBuf");
                let selectorGroups = document.getElementById("selectorGroups");
                let section = document.getElementsByTagName("section");
                let eleSection = section[0];
                selectorGroups.style.height = (document.scrollingElement.clientHeight - 100 - catalogUIBuf.offsetHeight -46) + "px"; //滚动条
                selectorGroups.style.paddingBottom = "46px";
                catalogUI.style.left = (eleSection.offsetLeft + eleSection.offsetWidth + 60) + "px"; //x位置
                let vTop = (eleSection.offsetTop - catalogUI.offsetHeight);
                catalogUI.style.top = 60 + "px"; //y位置

                /* 绑定选择器事件 */
                let selector1_a = document.querySelectorAll("#selector1 a"); //对局选择器 //name->id
                let selector2_a = document.querySelectorAll("#selector2 a"); //恶手选择器 //id
                let selector3_a = document.querySelectorAll("#selector3 a"); //不一致选择器 //name
                let differMap = new Map(); //不一致选择器

                let nameIdSelectorHandle = function(e) {
                    let idStr = e.target.name;
                    let targetNode = document.getElementById(idStr + "-main");
                    if(targetNode == null){
                        Console.orig.error("nameIdSelectorHandle", "获取到的目标节点为空!"); return;
                    }
                    if(targetNode.id.indexOf("kyoku") != -1){
                        let parentEle = targetNode.parentElement.parentElement.parentElement;
                        let changeEle = parentEle.getElementsByClassName("sticky")[0];
                        changeEle.style.background = "transparent";
                        CustomUtils.setHighlightShow(parentEle, 750, ()=>{
                            changeEle.style.background = "";
                        }); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode, "start");
                    }else{
                        CustomUtils.setHighlightShow(targetNode, 750); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode, "center");
                    }
                };
                let idSelectorHandle = function(e) {
                    let idStr = e.target.id;
                    let targetNode = document.getElementById(idStr + "-main");
                    if(targetNode == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                    }    
                    if(targetNode.id.indexOf("kyoku") != -1){
                        let parentEle = targetNode.parentElement.parentElement.parentElement;
                        let changeEle = parentEle.getElementsByClassName("sticky")[0];
                        changeEle.style.background = "transparent";
                        CustomUtils.setHighlightShow(parentEle, 750, ()=>{
                            changeEle.style.background = "";
                        }); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode, "start");
                    }else{
                        CustomUtils.setHighlightShow(targetNode, 750); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode, "center");
                    }
                };
                let nameSelectorHandle = function(e) {
                    let idStr = e.target.name;
                    let targetNode = document.getElementsByName(idStr + "-main");
                    if(targetNode == null){
                       Console.orig.error("nameSelectorHandle", "获取到的目标节点为空!"); return;
                    }
                    if(targetNode[0].id.indexOf("kyoku") != -1){
                        let parentEle = targetNode.parentElement.parentElement.parentElement;
                        let changeEle = parentEle.getElementsByClassName("sticky")[0];
                        changeEle.style.background = "transparent";
                        CustomUtils.setHighlightShow(parentEle, 750, ()=>{
                            changeEle.style.background = "";
                        }); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode[0], "start");
                    }else{
                        CustomUtils.setHighlightShow(targetNode[0], 750); //高亮显示
                        CustomUtils.setScrollToTargetNode(targetNode[0], "center");
                    }
                };
                //绑定 对局选择器
                for (let i = 0; i < selector1_a.length; i++) {
                    const ele = selector1_a[i];
                    ele.addEventListener('click', function(e){
                        MainApp.disable_kyokuChoose = true;
                        return idSelectorHandle(e);
                    });
                }
                //绑定 恶手选择器
                for (let i = 0; i < selector2_a.length; i++) {
                    const ele = selector2_a[i];
                    //
                    let idStr = ele.id; //
                    let targetNode = document.getElementById(idStr + "-main");
                    if(targetNode == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); break;
                    }
                    let parentEle = targetNode.parentElement.parentElement;

                    const tenhouData = parentEle.getElementsByTagName("iframe")[0].src;
                    const json = JSON.parse(decodeURI(tenhouData.substring(tenhouData.indexOf("{")))); //天凤对局数据
                    const kyoku = json.log[0][0][0]; //局数
                    const count = json.log[0][0][1]; //本场数

                    //东南西场 (1=东 2=南 3=西)
                    let curkyokuMode = MJCommonUtils.normalKyokuToKyokuMode(kyoku); //标准局数 转 东南西场 (1=东 2=南 3=西)

                    let textKyoku = MJCommonUtils.normalKyokuToTextKyoku(kyoku, curkyokuMode); //标准局数 转 文本局数 (0=>1: 东一) (6=>3: 南3)

                    let key = `${curkyokuMode}${textKyoku}${count}`;
                    let badChooseArray = MainApp.badChooseMap.get(key);
                    if(badChooseArray == undefined)
                        badChooseArray = [];
                    badChooseArray.push(ele.id);
                    MainApp.badChooseMap.set(key, badChooseArray);
                    //
                    ele.addEventListener('click', function(e){
                        MainApp.disable_badChoose = true;
                        return idSelectorHandle(e);
                    });
                }
                //绑定 不一致选择器
                for (let i = 0; i < selector3_a.length; i++) {
                    const ele = selector3_a[i];
                    //
                    let idStr = ele.name; //
                    let targetNode = document.getElementsByName(idStr + "-main");
                    if(targetNode == null){
                        Console.orig.error("nameSelectorHandle", "获取到的目标节点为空!"); break;
                    }
                    let parentEle = targetNode[0].parentElement.parentElement;

                    const tenhouData = parentEle.getElementsByTagName("iframe")[0].src;
                    const json = JSON.parse(decodeURI(tenhouData.substring(tenhouData.indexOf("{")))); //天凤对局数据
                    const kyoku = json.log[0][0][0]; //局数
                    const count = json.log[0][0][1]; //本场数

                    //东南西场 (1=东 2=南 3=西)
                    let curkyokuMode = MJCommonUtils.normalKyokuToKyokuMode(kyoku); //标准局数 转 东南西场 (1=东 2=南 3=西)

                    let textKyoku = MJCommonUtils.normalKyokuToTextKyoku(kyoku, curkyokuMode); //标准局数 转 文本局数 (0=>1: 东一) (6=>3: 南3)

                    let key = `${curkyokuMode}${textKyoku}${count}`;
                    let differArray = differMap.get(key);
                    if(differArray == undefined)
                        differArray = [];
                    differArray.push(ele.name);
                    differMap.set(key, differArray);
                    //
                    ele.addEventListener('click', function(e){
                        MainApp.disable_diffChoose = true;
                        return nameSelectorHandle(e);
                    });
                }

                /* 对局选择器和切换器 详细化 (添加格外标记) */
                //收集数据-恶手选择器
                let strArrayMap = new Map();
                const countObject = {__proto__: null,
                    badChooseNum: 0,
                    badChooseNumCustom: 0,
                    differNum: 0,
                };
                let badChooseType1 = strArray2.filter((obj) => {
                    return obj.badChooseType == 1;
                });
                let badChooseType2 = strArray2.filter((obj) => {
                    return obj.badChooseType == 2;
                });
                for (let i = 0; i < badChooseType1.length; i++) {
                    const ele = badChooseType1[i];
                    let idStr = ele.parentHref; //
                    let parentEle = document.getElementById(idStr + "-main");
                    if(parentEle == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                    }

                    let obj = strArrayMap.get(idStr + "-main");
                    if(obj == undefined){
                        obj = { ...countObject }; //复制对象
                    }

                    obj.badChooseNum++;
                    strArrayMap.set(idStr + "-main", obj);
                }
                for (let i = 0; i < badChooseType2.length; i++) {
                    const ele = badChooseType2[i];
                    let idStr = ele.parentHref; //
                    let parentEle = document.getElementById(idStr + "-main");
                    if(parentEle == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                    }

                    let obj = strArrayMap.get(idStr + "-main");
                    if(obj == undefined){
                        obj = { ...countObject }; //复制对象
                    }

                    obj.badChooseNumCustom++;
                    strArrayMap.set(idStr + "-main", obj);
                }
                //收集数据-不一致选择器
                for (let i = 0; i < strArray3.length; i++) {
                    const ele = strArray3[i];
                    let idStr = ele.parentHref; //
                    let parentEle = document.getElementById(idStr + "-main");
                    if(parentEle == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                    }

                    let obj = strArrayMap.get(idStr + "-main");
                    if(obj == undefined){
                        obj = { ...countObject }; //复制对象
                    }

                    obj.differNum++;
                    strArrayMap.set(idStr + "-main", obj);
                }
                // 处理数据
                for (let i = 0; i < strArray1.length; i++) {
                    const ele = strArray1[i];
                    let idStr = ele.nameID; //
                    let targetEle = document.getElementById(idStr);
                    if(targetEle == null){
                        Console.orig.error("idSelectorHandle", "获取到的目标节点为空!"); return;
                    }

                    let obj = strArrayMap.get(idStr + "-main");
                    if(obj != undefined){ //如果为undefined, 说明整局打的都与ai一致,无需处理
                        if(obj.differNum > 0){
                            Utils.addElementsByHTMLTemplateText(`<span class="${cssIDNP} layui-badge-dot l-129px"></span>`, targetEle);
                        }
                        if(obj.badChooseNum > 0){
                            Utils.addElementsByHTMLTemplateText(`<span class="${cssIDNP} layui-badge l-155px bgColor1">${obj.badChooseNum}</span>`, targetEle);
                        }
                        if(obj.badChooseNumCustom > 0){
                            Utils.addElementsByHTMLTemplateText(`<span class="${cssIDNP} layui-badge l-190px bgColor2">${obj.badChooseNumCustom}</span>`, targetEle);
                        }
                    }
                }

                //Console.orig.log("end MainApp.createCatalogUI()");
            });
            
        }
        //
        static async createSettingUI() {

        }

    }

    const app = new App();
})((typeof unsafeWindow != 'undefined' ? unsafeWindow : window));