Greasy Fork

Greasy Fork is available in English.

手机百度贴吧自动展开楼层

有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey

当前为 2022-06-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         手机百度贴吧自动展开楼层
// @namespace    http://tampermonkey.net/
// @homepage     http://greasyfork.icu/scripts/445657
// @version      3.2
// @description  有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey
// @author       voeoc
// @match        https://tieba.baidu.com/*
// @match        https://jump2.bdimg.com/*
// @match        https://tiebac.baidu.com/*
// @exclude      https://*/index
// @exclude      https://*/index*
// @exclude      https://*/f?*kw=*
// @connect      https://tieba.baidu.com/mg/o/getFloorData
// @connect      https://jump2.bdimg.com/mg/o/getFloorData
// @connect      https://tiebac.baidu.com/mg/o/getFloorData
// @icon         https://tieba.baidu.com/favicon.ico
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getResourceText
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    const STR_GMKEY_IS_DEBUG = "VOEOC_GMKEY_IS_DEBUG";

    const STR_ID_LZLPAGE = "VOEOC-ID-LZLPAGE";
    const STR_ID_LZLPAGEBACKGROUND = "VOEOC-ID-LZLPAGEBACKGROUND";
    const STR_CLASSNAME_LZLPAGEIFRAME = "VOEOC-CLASSNAME-LZLPAGEIFRAME";

    const STR_VOEOCMARK = "VOEOCMARK"; // 临时标记

    const STR_DEBUG_LABEL_ERROR = "error";

    const STR_LZL_PAGE_TRANSITION_DURATION = "0.2s";

    const GM = {
        /**@ts-ignore @type {Window} */
        unsafeWindow: unsafeWindow,
        /**@ts-ignore @type {function(string):HTMLStyleElement} */
        addStyle: GM_addStyle,
        /**@ts-ignore @type {function(any):function():void} */
        xmlhttpRequest: GM_xmlhttpRequest,
        /**@ts-ignore @type {function(string, any):any} */
        getValue: GM_getValue,
        /**@ts-ignore @type {function(string, any):void} */
        setValue: GM_setValue,
        /**@ts-ignore @type {function():void} */
        listValues: GM_listValues,
        /**@ts-ignore @type {function(string, function(), string):number} */
        registerMenuCommand: GM_registerMenuCommand,
        /**@ts-ignore @type {function(string):void} */
        unregisterMenuCommand: GM_unregisterMenuCommand,
        /**@ts-ignore @type {function(string):string} */
        getResourceText: GM_getResourceText,
    }

    let IS_DEBUG = GM.getValue(STR_GMKEY_IS_DEBUG, false); // 调试信息开关。需要手动编辑油猴插件的存储数据

    const VOEOC_REG = { // 自定义正则
        POSTPAGE: RegExp(`postPage\?(?=.*tid\=)(?=.*postAuthorId\=)(?=.*forumId\=)`, 'i'), // 评论页url
        LZLPAGE: RegExp(`lzlPage\?(?=.*floor\=)(?=.*pid\=)`, 'i'), // 楼中楼页url
        PBDATA: RegExp(`getPbData\?.*pn\=.*`, 'i'), // 通用页面数据url
    }

    const PAGE_TYPE = { // 页面类型
        UNKNOW: -1, // 未知
        MAINPAGE: 0, // 主页
        POSTPAGE: 1, // 评论页
        LZLPAGE: 2, // 楼中楼页
        TIEBAPAGE: 3, // 贴吧主页
    };

    // 简单判断当前页面的类型
    function getPageType() {
        /**
         * 网址解析
         * 贴吧页,样例:https://tieba.baidu.com/f?kw=百度
         * 帖子页,样例:
         * 1. https://tieba.baidu.com/f?kz=111111111
         * 2. https://tieba.baidu.com/p/111111111
         * hash在帖子中位置判断(样例)
         * 1.评论页(展开页面):#/postPage?tid=111111111&postAuthorId=9468343&forumId=141431&locateConfig=[]&source=a0-bpb-c111111111-d0-e0
         * 2.楼中楼页:#/lzlPage?tid=111111111&pid=897196650&floor=2&postAuthorId=9468343&forumId=141431
         */
        //
        let isTiePage = false;
        if (RegExp(`^/f`).test(window.location.pathname)) {
            if (GET_URL_ATTR(window.location.href, "kw")) { // 贴吧主页、或者某一个吧的页面
                return PAGE_TYPE.TIEBAPAGE;
            } else if (GET_URL_ATTR(window.location.href, "kz")) { // 贴子页
                isTiePage = true;
            }
        } else if (RegExp(`^/p`).test(window.location.pathname)) { // 贴子页
            isTiePage = true;
        }

        if (isTiePage) {
            let hash = window.location.hash;
            if (hash === "" || hash === "#/") {
                return PAGE_TYPE.MAINPAGE;
            } else if (VOEOC_REG.POSTPAGE.test(hash)) {
                return PAGE_TYPE.POSTPAGE;
            } else if (VOEOC_REG.LZLPAGE.test(hash)) {
                return PAGE_TYPE.LZLPAGE;
            }
        }
        DEBUGLOG("未知页面", STR_DEBUG_LABEL_ERROR);
        return PAGE_TYPE.UNKNOW;
    }

    // 提前处理评论页,加快速度
    if (getPageType() == PAGE_TYPE.POSTPAGE) {
        // 如果当前刷新加载的是评论页,则需要先打开主页面获取数据
        window.location.hash = "";
        window.location.reload();
    }

    // 单例管理器,将单例实例独立出去
    class SingleInstanceManager {
        static #INSTANCE_LIST = {};
        /**
         *
         * @param {any} classT 传入相关类型
         */
        static ORDER_SINGLE(classT) {
            let instance = SingleInstanceManager.#INSTANCE_LIST[classT];
            if (!instance) {
                instance = new classT();
                SingleInstanceManager.#INSTANCE_LIST[classT] = instance;
            }
            classT.getInstance = function () {
                return instance;
            }
        }

        /**
         * 对于每个classT,只能运行一次,应在对应的类构造函数加入
         * @param {any} classT 传入相关类型
         */
        static CHECK_SINGLE_INSTANCE(classT) {
            ASSERT(!SingleInstanceManager.#INSTANCE_LIST[classT], "非法构建,只允许一个实例,请调用getInstance()获取对象");
        }
    }

    class SWITCHLABEL {
        static LABEL_TYLE = {
            UNKNOW: 0,
            SWITCH: 1 // 切换开关,实际控制的也是一个布尔值
        }
        /**
         *
         * @param {number} type
         * @param {string} prefix
         * @param {Array<string>} switchlist
         */
        constructor(type, prefix, switchlist) {
            this.type = type;
            this.prefix = prefix;
            this.switchlist = switchlist;
        }
    }

    class SettingValue {
        static TYPE = {
            NUMBER: "number",
            BOOLEAN: "boolean",
            STRING: "string",
            UNDEFINED: "undefined",
        }
        static LINK_TYPE = {
            UNKNOW: -1,
            SWITCH: 0, // 将自己作为开关,控制连接的对象
            CONTROLLED: 1, // 被另一个开关控制
            OPPOSITE: 2, // 与其他连接值始终相反
            SAME: 3, // 与其他连接值始终相同
        }
        #key;
        get key() { return this.#key; }
        #value;
        set value(newValue) {
            let newValueType = typeof newValue;
            ASSERT(newValueType == this.#defaultValueType, `类型不匹配,需要${this.#defaultValueType}却传入${newValueType}`);
            GM.setValue(this.#key, newValue);
            if (this.#value == newValue) {
                return;
            }
            this.#value = newValue;
            if (this.#linkObj && this.#defaultValue == SettingValue.TYPE.BOOLEAN) { // 关联
                if (this.#linkType == SettingValue.LINK_TYPE.OPPOSITE) {
                    if (this.#linkObj.value == this.#value) {
                        this.#linkObj.value = !this.#value;
                    }
                } else if (this.#linkObj.value != this.#value) {
                    this.#linkObj.value = this.#value;
                }
            }
        }
        get value() { return this.#value; }
        #defaultValue;
        get defaultValue() { return this.#defaultValue; }
        #defaultValueType;
        get defaultValueType() { return this.#defaultValueType; }
        /**@type { {min: number;max: number;} | undefined} 数字范围 */
        #range;
        get range() { return this.#range; }
        checkRange(value, self = this) {
            if (!self.#range || !self.#range.min || !self.#range.max) {
                return true;
            }
            return self.#range.min <= value && value <= self.#range.max;
        }

        /**@type {SWITCHLABEL | string} 标签说明*/
        #label;
        get label() {
            return this.#label;
        }

        /** @type SettingValue */
        #linkObj;
        get linkObj() { return this.#linkObj; }
        #linkType = SettingValue.LINK_TYPE.UNKNOW;
        get linkType() { return this.#linkType; }
        /**
         * 与另一个设置值对象的连接,任意一个对象执行一次link即可,
         * 1.当自己为布尔类型时,而令一个对象非布尔类型,将自身作为这另一个连接对象的开关;
         * 2.当自己为布尔类型时,而令一个对象也为布尔类型,则值变更时同时变更,如果相反开关存在则他们一直会相反;
         *
         * @param {SettingValue} otherObj
         * @param {number} linkType
         */
        linkOther(otherObj, linkType = SettingValue.LINK_TYPE.UNKNOW) {
            this.#linkObj = otherObj;
            this.#linkType = linkType;
            if (otherObj.linkObj != this) {
                switch (linkType) {
                    case SettingValue.LINK_TYPE.SWITCH:
                        otherObj.linkOther(this, SettingValue.LINK_TYPE.CONTROLLED);
                        break;
                    case SettingValue.LINK_TYPE.CONTROLLED:
                        otherObj.linkOther(this, SettingValue.LINK_TYPE.SWITCH);
                        break;
                    default:
                        otherObj.linkOther(this, linkType);
                        break;
                }
            }
        }

        /** @type {function(any):any} */
        parseFunc; // 自带解析函数,将外部数据转换
        /**
         *
         * @param {string} key 键
         * @param {any} defaultValue 默认值
         * @param {SWITCHLABEL | string} label 选择器标签
         * @param { {min: number;max: number;} } [range] 数字的范围
         */
        constructor(key, defaultValue, label, range = undefined) {
            let self = this;
            self.#key = key;
            self.#defaultValue = defaultValue;
            self.#defaultValueType = typeof defaultValue;
            self.#range = range;

            self.#label = label;

            switch (self.#defaultValueType) {
                case SettingValue.TYPE.STRING:
                    self.parseFunc = String;
                    break;
                case SettingValue.TYPE.NUMBER:
                    self.parseFunc = parseInt;
                    break;
                case SettingValue.TYPE.BOOLEAN:
                    self.parseFunc = Boolean;
                    break;
                default:
                    throw new Error("不支持该类型");
                    break;
            }

            self.loadValue();
        }

        loadValue(self = this) {
            let gValue = GM.getValue(self.#key, self.#defaultValue);
            if (typeof self.#defaultValue !== typeof gValue ||
                Number.isNaN(gValue) ||
                !self.checkRange(gValue)) {
                DEBUGLOG(`存储的值(${gValue})格式不对,执行重设`, STR_DEBUG_LABEL_ERROR);
                // 存储的值格式不对,执行重设
                self.value = self.#defaultValue;
            } else {
                // 读取正常值
                self.#value = gValue;
            }
        }
    }

    const HTML_SVG_CLOSE_BTN = `<svg t="1655203632724" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2322" width="22" height="22"><path d="M576 512l277.333333 277.333333-64 64-277.333333-277.333333L234.666667 853.333333 170.666667 789.333333l277.333333-277.333333L170.666667 234.666667 234.666667 170.666667l277.333333 277.333333L789.333333 170.666667 853.333333 234.666667 576 512z" fill="#444444" p-id="2323"></path></svg>`;
    const HTML_SVG_RELOAD_BTN = `<svg t="1655490109919" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2737" width="22" height="22"><path d="M901.1 629.4c-51.6 159-203.4 274.1-382.7 274.1-221.8 0-401.5-176.2-401.5-393.6 0-217.4 179.8-393.6 401.5-393.6 131.4 0 248 61.8 321.2 157.4H679c-14.8 0-26.8 11.8-26.8 26.2s12 26.2 26.8 26.2h214.1c14.8 0 26.8-11.7 26.8-26.2V90.1c0-14.5-12-26.2-26.8-26.2s-26.8 11.7-26.8 26.2v132.4c-83.5-97-208.4-158.7-348-158.7-251.2 0.1-455 199.8-455 446.2 0 246.4 203.7 446.1 455.1 446.1 207.3 0 382.2-135.9 437.1-321.7-15.9-14.2-39-20.7-54.4-5z" fill="#040404" p-id="2738"></path></svg>`;
    const HTML_SVG_SETTING_BTN = `<svg t="1655732503110" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2875" width="12" height="12"><path d="M424.763392 689.588224l259.528704-259.524608c73.37984 37.507072 165.515264 25.591808 226.932736-35.823616 61.417472-61.41952 73.332736-153.552896 35.825664-226.934784l-168.658944 168.656896-85.05344-85.055488 168.654848-168.656896c-73.37984-37.50912-165.515264-25.593856-226.932736 35.823616-61.417472 61.417472-73.332736 153.552896-35.825664 226.934784L339.707904 604.532736c-73.37984-37.507072-165.515264-25.591808-226.932736 35.823616C51.359744 701.771776 39.44448 793.9072 76.949504 867.291136l168.656896-168.656896 85.057536 85.055488-168.656896 168.656896c73.383936 37.505024 165.517312 25.591808 226.936832-35.827712C450.357248 855.103488 462.27456 762.97216 424.763392 689.588224z" p-id="2876" fill="#121212"></path></svg>`;
    const HTML_SVG_DOWN_BTN = `<svg t="1655795713828" class="icon" viewBox="0 0 1902 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2448" width="18" height="18"><path d="M155.204535 74.710455C126.615552 46.121472 80.483035 45.902117 51.719826 74.665326L51.719826 74.665326C23.155712 103.22944 23.012571 149.397723 51.378322 177.763474L827.770953 954.156105C884.758821 1011.143973 977.122889 1011.175424 1034.142208 954.156105L1810.534839 177.763474C1838.910318 149.387995 1838.956544 103.428535 1810.193335 74.665326L1810.193335 74.665326C1781.629294 46.101285 1735.477321 45.94176 1706.708626 74.710455L982.721463 798.697618C954.132553 827.286601 907.960393 827.466313 879.191698 798.697618L155.204535 74.710455 155.204535 74.710455Z" p-id="2449" fill="#121212"></path></svg>`;

    // 载入配置
    const SETTINGS_DATA = {
        CHTML_0: `<span>设置(手动刷新后生效)</span><hr>`,
        // 设置展开按钮点击动作
        isClickToExpandLzlPage: new SettingValue("VOEOC_GMKEY_isClickToExpandLzlPage", true,
            new SWITCHLABEL(SWITCHLABEL.LABEL_TYLE.SWITCH, "单击展开按钮时", ["原地展开", "弹出楼中楼页"])),
        // 设置展开按钮长按动作
        isLongClickToExpandLzlPage: new SettingValue("VOEOC_GMKEY_isLongClickToExpandLzlPage", false,
            new SWITCHLABEL(SWITCHLABEL.LABEL_TYLE.SWITCH, "长按展开按钮时", ["原地展开", "弹出楼中楼页"])),
        CHTML_1: "<hr>",
        /**@todo*/
        //isSeeLzOnly: new SettingValue("VOEOC_GMKEY_isCanSeeLzOnly", true, "只看楼主"),

        isAutoExpand: new SettingValue("VOEOC_GMKEY_isAutoExpand", true, "载入时自动展开楼中楼"), // 自动展开的开关
        isRemaindAutoExpand: new SettingValue("VOEOC_GMKEY_isRemaindAutoExpand", true, "剩余评论过少时展开楼中楼"), // 剩余评论过少时自动展开的开关
        CHTML_2: "<hr>",
        eachExpandSize: new SettingValue("VOEOC_GMKEY_eachExpandSize", 10, "楼中楼一次展开的行数", { min: 10, max: 30 }), // 每次展开的评论数量,至少为10,少于10按10计算
        autoExpandNum: new SettingValue("VOEOC_GMKEY_autoExpandNum", 1, "自动展开次数", { min: 1, max: 5 }), // 每次展开的评论数量,至少为10,少于10按10计算
        remaindAutoExpandSize: new SettingValue("VOEOC_GMKEY_remaindAutoExpandSize", 7, "剩余评论自动展开", { min: 0, max: 20 }), // 当剩余评论少于这个数时,执行自动展开
        lzlCacheSize: new SettingValue("VOEOC_GMKEY_lzlCacheSize", 10, "楼中楼页缓存数量", { min: 1, max: 40 }), // 楼中楼页缓存的大小
    }
    SETTINGS_DATA.isRemaindAutoExpand.linkOther(SETTINGS_DATA.remaindAutoExpandSize, SettingValue.LINK_TYPE.SWITCH);
    SETTINGS_DATA.isAutoExpand.linkOther(SETTINGS_DATA.autoExpandNum, SettingValue.LINK_TYPE.SWITCH);
    //settingsData.isClickToExpandLzlPage.linkOther(settingsData.isLongClickToExpandLzlPage, SettingValue.LINK_TYPE.OPPOSITE);

    function CREATE_CSS_TEXT(str) {
        return str.replace(/\n/g, "");
    }
    const STR_CSS_REMOVE = CREATE_CSS_TEXT(
        `
.comment-box, .only-lz, .nav-bar-bottom, .open-app, .more-image-desc {
display: none !important;
}
.logo-wrapper {
visibility: hidden !important;
pointer-events: none !important;
height: 0;
}
.open-app-text {
display: none !important;
}
`);

    const STR_CSS_MAINPAGE = CREATE_CSS_TEXT(
        `
.open-app-text-real {
display: block !important;
-webkit-box-flex: 0;
-webkit-flex: none;
-ms-flex: none;
flex: none;
font-size: .13rem;
color: #614ec2;
}
.open-app-text-real.error {
color: #ff3366 !important;
text-decoration: line-through;
}
@keyframes rotate3d {
0%{-webkit-transform:rotate3d(1, 0, 0, 0deg);}
25%{-webkit-transform:rotate3d(1, 0, 0, 90deg);}
50%{-webkit-transform:rotate3d(1, 0, 0, 180deg);}
75%{-webkit-transform:rotate3d(1, 0, 0, 270deg);}
100%{-webkit-transform:rotate3d(1, 0, 0, 360deg);}
}
.open-app-guide.loading {
animation: rotate3d 0.5s linear infinite;
pointer-events: none;
}
.voeoc-swal-input {
position: center;
}
.voeoc-swal-input-checkbox {
height: 0.15rem;
width: 0.15rem;
}
.voeoc-swal-range {
width: 100%;
}
.voeoc-swal-range-label {
font-size: 0.13rem;
}
div.disabled > .voeoc-swal-range-label {
color: #939393ab;;
}
.switch_label {
text-decoration: line-through;
color: gray;
padding: 0.05rem;
border-radius: 0.05rem;
line-height: 0.15rem !important;
}
.switch_label.on {
text-decoration: unset;
color: #eeeded;
background-color: #3480e5ab;
}
.switch_checkbox {}
.switch_checkbox > * {
display: inline-block;
}
.swal2-content {
font-size: 0.15rem !important;
line-height: 0.4rem !important;
color: gainsboro;
}
.swal2-popup {
font-size: unset;
}
#${STR_ID_LZLPAGE} {
position: fixed;
overscroll-behavior: none;
width: 100%;
z-index: 999;
height: 0;
visibility: hidden;
transition: visibility ${STR_LZL_PAGE_TRANSITION_DURATION};
overflow: scroll;
}
#${STR_ID_LZLPAGE}::-webkit-scrollbar { width: 0 !important }
#${STR_ID_LZLPAGEBACKGROUND} {
position: fixed;
width: 100%;
height: 200%;
background-color: #00000077;
z-index: -1;
opacity: 0;
transition: opacity ${STR_LZL_PAGE_TRANSITION_DURATION};
}
.${STR_CLASSNAME_LZLPAGEIFRAME} {
position: fixed;
width: 100%;
height: 0%;
background-color: #ffffff;
bottom: 0;
transition: height ${STR_LZL_PAGE_TRANSITION_DURATION};
overflow: hidden;
}
.lzl-nav-btn.lzl-reload-btn {
position:fixed;
margin: 0.1rem;
top:20%;
margin-left: -0.5rem;
transition: margin-left ${STR_LZL_PAGE_TRANSITION_DURATION};
}
#${STR_ID_LZLPAGE}.show {
display: block;
height: 100%;
visibility: visible;
}
#${STR_ID_LZLPAGE}.loading, #${STR_ID_LZLPAGE}.loading > .${STR_CLASSNAME_LZLPAGEIFRAME} {
pointer-events: none;
}
#${STR_ID_LZLPAGE}.show > .${STR_CLASSNAME_LZLPAGEIFRAME}.show {
height: 80%;
}
#${STR_ID_LZLPAGE}.show > #${STR_ID_LZLPAGEBACKGROUND} {
opacity: 1;
}
#${STR_ID_LZLPAGE}.show > .lzl-reload-btn {
margin-left: 0.1rem;
}
.lzl-nav-btn {
width: .32rem;
height: .32rem;
display: flex;
background-color:#d0d0d04a;
border-radius:50%;
}
.lzl-nav-btn > svg{
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@keyframes rotate {
0%{-webkit-transform:rotate(0deg);}
25%{-webkit-transform:rotate(90deg);}
50%{-webkit-transform:rotate(180deg);}
75%{-webkit-transform:rotate(270deg);}
100%{-webkit-transform:rotate(360deg);}
}
#${STR_ID_LZLPAGE}.loading > .lzl-reload-btn {
animation: rotate 0.2s linear infinite;
pointer-events: none;
}
#${STR_ID_LZLPAGE}.error > .lzl-reload-btn {
background-color: #ff000070 !important;
}
.slide-down-btn{
position: fixed;
top: 20%;
left: 50%;
margin-left: -0.32rem;
opacity: 0;
transition: opacity,margin-top,background-color 0.1s;
}
.slide-down-btn.show{
opacity: 1;
}
.slide-down-btn.confirm{
background-color: #0000004a;
}
.voeoc-setting-dialog {
height: 0;
width: 100%;
position: fixed;
z-index: 9999;
background-color: #000000d1;
color: #eee;
overflow-y: auto;
justify-content: center;
top: 0;
right: 0;
bottom: 0;
left: 0;
flex-direction: row;
display: flex;
overflow-x: hidden;
transition: background-color .1s;
overscroll-behavior: none;
}
.voeoc-setting-dialog.show{
height: 100%;
}
.voeoc-settting-content {
width: 100%;
height: fit-content;
min-height: 100%;
padding-bottom: 0.5rem;
}
.voeoc-settting-value-list {
text-align: left;
padding: 0.2rem;
line-height: 0.4rem !important;
}
.voeoc-setting-button-pad {
margin:0 auto;
width: fit-content;
float: right;
margin-right: 0.13rem;
}
.voeoc-setting-button-pad > * {
display: inline-block;
padding: 0.01rem;
margin: 0.05rem;
}
.voeoc-setting-button {
background: #7d7d7d;
width: fit-content;
padding-inline: 0.3rem;
padding-block: 0.05rem;
border-radius: 0.03rem; 
}
`);

    const HISTORY_STATE = {
        LZLPAGE: {
            title: "楼中楼回复",
            id: "1"
        },
        SETTINGDIALOG: {
            title: "设置窗口",
            id: "2"
        },
    }

    class VoeocDialog {
        /**
         * @protect
         * @type {{title: string,id: string}} */
        historyState;
        /**
         * @protect
         * @type {HTMLDivElement}
         */
        dialogNode;

        show(self = this) {
            self.dialogNode.classList.add("show");
        }

        hide(self = this) {
            self.dialogNode.classList.remove("show");
        }

        isShown(self = this) {
            return self.dialogNode.classList.contains("show");
        }

        open(self = this) {
            HistoryStateManager.getInstance().showNewDialog(self);
        }
        close() {
            HistoryStateManager.getInstance().closeLastDialog();
        }

        /**
         * @param {{ title: string; id: string; }} historyState
         * @param {HTMLDivElement} dialogNode
         */
        setState(historyState, dialogNode) {
            this.historyState = historyState;
            this.dialogNode = dialogNode;
        }
    }

    class HistoryStateManager {

        /**@type {Stack<VoeocDialog>}*/
        #showDialogList;

        /**@returns {HistoryStateManager} */
        static getInstance() {
            throw new Error("请使用SingleInstanceManager初始化");
        }

        constructor() {
            SingleInstanceManager.CHECK_SINGLE_INSTANCE(HistoryStateManager);
        }

        init(self = this) {
            self.#showDialogList = new Stack();

            // 监听后退按钮
            window.addEventListener("popstate", function (event) {
                if (self.#showDialogList.isEmpty()) {
                    return;
                }
                DEBUGLOG(window.history.state.id);
                if (self.#showDialogList.peek().isShown()) {
                    self.closeLastDialog();
                }
            }, false);
        }

        /**
         * 
         * @param {VoeocDialog} voeocDialog 
         */
        showNewDialog(voeocDialog, self = this) {
            if (!voeocDialog.isShown()) {
                voeocDialog.show();
                window.history.pushState(voeocDialog.historyState, voeocDialog.historyState.title);
                self.#showDialogList.push(voeocDialog);
            }
        }

        closeLastDialog(self = this) {
            if (self.#showDialogList.isEmpty()) {
                return;
            }
            let hiddenDialog = self.#showDialogList.peek();
            if (hiddenDialog.isShown()) {
                if (window.history.state.id == hiddenDialog?.historyState.id) { // 当前历史记录的state没有改变,说明事件不是后退键触发的
                    window.history.back(); // 模拟后退键,改变当前页面的state
                    return;
                }
                // 正式隐藏
                hiddenDialog?.hide();
                self.#showDialogList.pop();
            }
        }
    }

    // 获取屏幕DPI
    const DPI = (() => {
        let DPI = {
            x: 160,
            y: 160,
        };
        let tmpNode = document.createElement("DIV");
        tmpNode.style.cssText = "width:1in;height:1in;position:absolute;left:0px;top:0px;z-index:99;visibility:hidden";
        document.body.appendChild(tmpNode);
        DPI.x = tmpNode.offsetWidth;
        DPI.y = tmpNode.offsetHeight;
        document.body.removeChild(tmpNode);
        return DPI;
    })();

    /**
     * 将厘米转换为像素
     * @param {number} cm 厘米数
     * @param {number} dpi 每英寸像素点数
     * @returns 像素
     */
    function CM2PX(cm, dpi = DPI.y) {
        return (cm * dpi) / 25.4;
    }

    /**
     * 自动输出错误
     * @param {function() : void} func
     * @param {string} [msg]
     */
    function AUTO_CATCH_ERROR(func, msg = undefined) {
        try {
            func();
        } catch (e) {
            if (!msg) {
                msg = e;
            }
            DEBUGLOG(msg, STR_DEBUG_LABEL_ERROR);
        }
    }

    /**
     * 当不符合条件时自动中断
     * @param {any} condition 条件
     * @param {string} msg 错误信息
     */
    function ASSERT(condition, msg) {
        if (!condition) {
            throw new Error(`${msg}`)
        }
    }

    /**
     * 输出调试信息
     * @param {any} msg 信息
     * @param {string} label 标签
     */
    function DEBUGLOG(msg, label = "") {
        if (!IS_DEBUG) {
            return;
        }
        let outputFunc = console.log;
        if (label == STR_DEBUG_LABEL_ERROR) {
            outputFunc = console.error;
        }
        outputFunc(`voeoc(DEBUG)<${label}>: ${msg}`);
    }

    /**
     * 阻止事件冒泡
     * @param {Event} event
     */
    function STOPPROPAGATION(event) {
        event = event || window.event;
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
        return false;
    }

    function REGISTER_MENUCOMMAND() {
        let menuId = GM.registerMenuCommand(`设置`, SettingDialog.OPEN_DIALOG, "VOEOC_MENU_ACCESS_KEY_SETTINGS");
    }

    /**
     *
     * @param {string} selector  css选择器
     * @param {number} TIME_OUT 查找的次数
     * @param {function(string): any} searchFunc 搜索函数
     */
    function WAIT_ELEMENT_LOADED_ASYNC(selector, TIME_OUT = 30, searchFunc = document.querySelector.bind(document)) {
        return new Promise((resolve, reject) => {
            let findTimeNum = 0; // 记录查找的次数
            let timer = setInterval(() => {
                let element = searchFunc(selector);
                DEBUGLOG(`${selector}=${element}`, "waitElementLoaded");
                if (element != null) {
                    // 清除定时器
                    clearInterval(timer);
                    resolve(element);
                    return;
                }
                findTimeNum++;
                if (TIME_OUT < findTimeNum) { // 超过设定次数
                    // 清除定时器
                    clearInterval(timer);
                    reject(new Error(`${selector}=${element}`));
                }
            }, 200);
        });
    }

    /**
     *
     * @param {string} selector css选择器
     * @param {function(HTMLElement): void} func 找出元素后的后续操作
     * @param {number} TIME_OUT 查找的次数
     * @param {function(string): any} searchFunc 搜索函数
     * @param {function(): void} [finalFunc] 出错后执行的函数
     */
    function WAIT_ELEMENT_LOADED(selector, func, TIME_OUT = 30, searchFunc = document.querySelector.bind(document), finalFunc = undefined) {
        WAIT_ELEMENT_LOADED_ASYNC(selector, TIME_OUT, searchFunc).then(func, function (error) {
            if (finalFunc) {
                finalFunc;
            }
        });
    }

    /**
     * 等待文档渲染完成
     * @param {function(): void} func
     */
    function WAIT_DOCUMENT_READY(func, documentNode = document) {
        WAIT_ELEMENT_LOADED(`${documentNode.nodeName} readyState`, func, 10, function () {
            if (documentNode.readyState == "complete") {
                return true;
            }
            return undefined;
        })
    }


    /**
     * 获取url参数
     * @param {string} url
     * @param {string} attrName
     * @returns {string | undefined} 参数值
     */
    function GET_URL_ATTR(url, attrName) {
        return MATCH_REG(RegExp(`${attrName}=([^&]*)&?`, 'i'), url);
    }

    /**
     * 匹配正则表达式
     * @param {RegExp} regExp
     * @param {string} str
     * @returns {string | undefined} 第一个符合需求的字符串
     */
    function MATCH_REG(regExp, str) {
        let regExpMatchArray = regExp.exec(str);
        if (regExpMatchArray && regExpMatchArray.length > 1) {
            return regExpMatchArray[1].trim();
        }
        return undefined;
    }

    /**
     * @param {string} cssText
     */
    function INSERT_CSS(cssText, documentNode = document) {
        let newStyleNode = documentNode.createElement('style');
        try {
            newStyleNode.appendChild(documentNode.createTextNode(cssText));
        } catch (e) {
            DEBUGLOG(e, STR_DEBUG_LABEL_ERROR);
            // @ts-ignore
            newStyleNode.rel = 'stylesheet';
            // @ts-ignore
            newStyleNode.styleSheet.cssText = cssText;
        }
        let head = documentNode.getElementsByTagName('head')[0];
        head.appendChild(newStyleNode);
        WAIT_DOCUMENT_READY(function () {

        }, documentNode)
    }

    class SettingDialog extends VoeocDialog {
        /**
         * 生成复选框
         * @param {SettingValue} settingValue
         * @param {string} onchangeFunc
         * @param {string} cssText
         * @returns 返回生成的HTML
         */
        static #generateHTMLCheckbox(settingValue, onchangeFunc = "", cssText = "") {
            let checked = settingValue.value ? "checked" : "";
            return `<input class="voeoc-swal-input-checkbox" type="checkbox"id="${settingValue.key}"style="${cssText}" ${checked} value="${checked}" onload="onchange()"onchange="${onchangeFunc}">`;
        }
        /**
         * 生成数字输入器
         * @param {SettingValue} settingValue
         * @returns 返回生成的HTML
         */
        static #generateHTMLNumberInputbox(settingValue) {
            let id_show_value = `${settingValue.key}-show`;
            let disabled = (settingValue.linkObj &&
                settingValue.linkObj.defaultValueType == SettingValue.TYPE.BOOLEAN &&
                !settingValue.linkObj.value);
            let min = undefined;
            let max = undefined;
            if (settingValue.range) {
                min = settingValue.range.min;
                max = settingValue.range.max;
            }

            return `<div class="${disabled ? "disabled" : ""}"><span class="voeoc-swal-range-label">${settingValue.label}(<span class="voeoc-swal-range-label" id='${id_show_value}'>${settingValue.value}</span>):</span><br><input class="voeoc-swal-range" type="range" min="${min}" max="${max}" step="1"id="${settingValue.key}" ${disabled ? "disabled='true'" : ""}value="${settingValue.value}"onload="onchange()"oninput="onchange()"onchange="document.getElementById('${id_show_value}').innerHTML=this.value;"></div>`;
        }
        /**
         *
         * @param {SettingValue} settingValue
         */
        static #getInputTrueValue(settingValue) {
            AUTO_CATCH_ERROR(() => {
                // @ts-ignore
                let newValue = document.getElementById(settingValue.key).value;
                settingValue.value = settingValue.parseFunc(newValue);
            });
        }
        static OPEN_DIALOG() {
            // 重新刷新存储的配置,应用到窗口上
            // for (let key in SETTINGS_DATA) {
            //     let settingValue = SETTINGS_DATA[key];
            //     if (settingValue instanceof SettingValue) {
            //         settingValue.loadValue();
            //     }
            // }

            // 重新显示窗口
            SettingDialog.getInstance().open();
        }

        /**@returns {SettingDialog} */
        static getInstance() {
            throw new Error("请使用SingleInstanceManager初始化");
        }

        constructor() {
            SingleInstanceManager.CHECK_SINGLE_INSTANCE(SettingDialog);
            super();
        }

        /**@type {HTMLDivElement} */
        #settingDialogNode;
        /**@type {HTMLDivElement} */
        #settingDialogValueListNode;
        init(self = this) {
            // 生成对话框数据内容节点
            self.#settingDialogValueListNode = document.createElement("div");
            self.#settingDialogValueListNode.className = "voeoc-settting-value-list";
            let settingvaluelisthtml = "";
            for (let key in SETTINGS_DATA) {
                let settingValue = SETTINGS_DATA[key];
                if (settingValue instanceof SettingValue) {
                    AUTO_CATCH_ERROR(() => {
                        /**@type {SettingValue} */
                        switch (settingValue.defaultValueType) {
                            case SettingValue.TYPE.NUMBER:
                                settingvaluelisthtml += SettingDialog.#generateHTMLNumberInputbox(settingValue);
                                break;
                            case SettingValue.TYPE.BOOLEAN:
                                let onchangeFunc = "if((this.value=='checked')!=this.checked){this.value=this.checked?'checked':'';}else{return;}";

                                let linkFunc = "";
                                if (settingValue.linkObj) {
                                    linkFunc = `let linkNode = document.getElementById('${settingValue.linkObj.key}');`
                                    switch (settingValue.linkObj.defaultValueType) {
                                        case SettingValue.TYPE.BOOLEAN:
                                            if (settingValue.linkType == SettingValue.LINK_TYPE.OPPOSITE || settingValue.linkType == SettingValue.LINK_TYPE.SAME) {
                                                linkFunc += `linkNode.checked=${settingValue.linkType == SettingValue.LINK_TYPE.OPPOSITE ? "!" : ""}this.checked;linkNode.onchange();`;
                                                break;
                                            }
                                        case SettingValue.TYPE.NUMBER:
                                            linkFunc += "linkNode.disabled=!this.checked;if(this.checked){linkNode.parentNode.classList.remove('disabled')}else{linkNode.parentNode.classList.add('disabled')}";
                                            break;
                                        case SettingValue.TYPE.STRING:
                                        default:
                                            throw new Error("不支持该类型");
                                            break;
                                    }
                                }
                                onchangeFunc += linkFunc;

                                if (settingValue.label instanceof SWITCHLABEL &&
                                    settingValue.label.type == SWITCHLABEL.LABEL_TYLE.SWITCH) {
                                    onchangeFunc += `let stateList = [];let indexOn = this.checked ? 0 : 1;for(let i=0; i<2; ++i) {let stateLabel = document.getElementById('${settingValue.key}-'+i);if(i == indexOn) {stateLabel.classList.add('on');} else {stateLabel.classList.remove('on');}}`;
                                    settingvaluelisthtml += "<div class=\"switch_checkbox\" onclick=\"this.getElementsByTagName(\'input\')[0].click()\">";
                                    settingvaluelisthtml += SettingDialog.#generateHTMLCheckbox(settingValue, onchangeFunc, "visibility:hidden");
                                    settingvaluelisthtml += `<label for="${settingValue.key}">${settingValue.label.prefix}</label>`;

                                    let indexOn = settingValue.value ? 0 : 1;
                                    for (let i = 0; i < 2; ++i) {
                                        settingvaluelisthtml += `<div id="${settingValue.key}-${i}" class="switch_label${i == indexOn ? " on" : ""}"><label for="${settingValue.key}">${settingValue.label.switchlist[i]}</label></div>`;
                                    }
                                    settingvaluelisthtml += "</div>";
                                } else {
                                    settingvaluelisthtml += "<div onclick=\"this.getElementsByTagName(\'input\')[0].click()\">";
                                    settingvaluelisthtml += SettingDialog.#generateHTMLCheckbox(settingValue, onchangeFunc);
                                    settingvaluelisthtml += `<label for="${settingValue.key}">${settingValue.label}</label>`;
                                    settingvaluelisthtml += "</div>";
                                }
                                break;
                            case SettingValue.TYPE.STRING:
                            default:
                                throw new Error("不支持该类型");
                                break;
                        }
                    })
                } else if (typeof settingValue == "string") {
                    settingvaluelisthtml += settingValue;
                }
            }
            self.#settingDialogValueListNode.innerHTML = settingvaluelisthtml.replace(/\n/g, "");

            // 确认并刷新按钮
            let settingDialogButtonConfirmAndReload = document.createElement("div");
            settingDialogButtonConfirmAndReload.className = "voeoc-setting-button";
            settingDialogButtonConfirmAndReload.style.cssText = "background: #4f42e6;";
            settingDialogButtonConfirmAndReload.innerText = "保存并刷新";
            // 确认按钮
            let settingDialogButtonConfirm = document.createElement("div");
            settingDialogButtonConfirm.className = "voeoc-setting-button";
            settingDialogButtonConfirm.style.cssText = "background: #4289e6;";
            settingDialogButtonConfirm.innerText = "保存";
            // 取消按钮
            let settingDialogButtonCancel = document.createElement("div");
            settingDialogButtonCancel.className = "voeoc-setting-button";
            settingDialogButtonCancel.innerText = "取消";

            // 处理按钮面板
            let settingDialogButtonPad = document.createElement("div");
            settingDialogButtonPad.className = "voeoc-setting-button-pad";
            settingDialogButtonPad.appendChild(settingDialogButtonConfirmAndReload);
            settingDialogButtonPad.appendChild(settingDialogButtonConfirm);
            settingDialogButtonPad.appendChild(settingDialogButtonCancel);

            // 事件处理
            settingDialogButtonCancel.onclick = function () {
                self.close();
            };
            settingDialogButtonConfirm.onclick = function () {
                self.#getAllDialogValue();
                self.close();
            }
            settingDialogButtonConfirmAndReload.onclick = function () {
                self.#getAllDialogValue();
                window.location.hash = "";
                window.location.reload();
            }

            let settingDialogContentNode = document.createElement("div");
            settingDialogContentNode.className = "voeoc-settting-content";
            settingDialogContentNode.appendChild(self.#settingDialogValueListNode);
            settingDialogContentNode.appendChild(settingDialogButtonPad);

            // 对话框节点
            self.#settingDialogNode = document.createElement("div");
            self.#settingDialogNode.className = "voeoc-setting-dialog";
            self.#settingDialogNode.appendChild(settingDialogContentNode);

            document.body.appendChild(self.#settingDialogNode);
            
            
            super.setState(HISTORY_STATE.SETTINGDIALOG, self.#settingDialogNode);
        }

        #getAllDialogValue() {
            for (let key in SETTINGS_DATA) {
                if (SETTINGS_DATA[key] instanceof SettingValue) {
                    if (SETTINGS_DATA[key].label instanceof Array) { // 含有多个label
                    }
                    SettingDialog.#getInputTrueValue(SETTINGS_DATA[key]);
                }
            }
        }

    }

    class CustomLzlExpandManager { // 实现楼中楼展开的逻辑管理器
        static #STR_NEWOPENLZLTEXT = "展开评论"; // 打开楼中楼按钮的文本
        static #STR_REMAINDOPENLZLTEXT = function (num) {
            return `剩余${num}个评论`;
        }
        static #LZL_CONTENT_TYPE = { // 楼中楼评论内容元素类型
            TEXT: 0, // 文本
            EMOJI: 2, // 表情
            USERNAME: 4, // 用户名,一般用作回复
        };

        #enable; // 按钮开关
        #currentPageNum; // 当前展开页,用于网络请求
        #pageSize; // 单个页面评论数量,至少为10
        #originItemNodeList; // 原始楼中楼评论显示节点
        #sampleItemNode; // 原始楼中楼评论样本
        #lzTagHTML; // 一个楼主方框标记
        #data_v_a; // 评论中的第一个dataset数据(data-v-***),用于还原样式
        #data_v_b; // 评论中的第二个dataset数据(data-v-***),用于还原样式
        #pid; // 楼层id
        #floorNum; // 当前楼层的楼层数
        #expandNum; // 记录当前展开的次数
        get floorNum() { return this.#floorNum; }
        // 存储的网页节点
        #floorNode; // 当前楼层
        #lzlNode; // 楼中楼
        #expandBtnNode; // 楼中楼展开按钮
        #expandBtnTextNode; // 楼中楼展开按钮的文本

        #expandTimeoutTimer; // 超时处理器

        /**
         *
         * @param {HTMLElement} floorNode
         * @param {HTMLElement} expandBtnNode
         * @param {string} floorNum
         * @param {string} pid
         */
        constructor(floorNode, expandBtnNode, floorNum, pid) {
            ASSERT(floorNode, "floorNode is null");
            ASSERT(expandBtnNode, "expandBtnNode is null");
            let self = this;
            self.#enable = true;
            self.#currentPageNum = 1;
            self.#pageSize = SETTINGS_DATA.eachExpandSize.value < 10 ? 10 : SETTINGS_DATA.eachExpandSize.value; // 单个展开的页面评论数量,至少为10
            self.#floorNode = floorNode; // 当前楼层节点
            self.#expandBtnNode = expandBtnNode; // 楼中楼展开按钮
            self.#lzlNode = self.#floorNode.querySelector("div.lzl-post");

            self.#originItemNodeList = self.#lzlNode.getElementsByClassName("lzl-post-item");
            self.#sampleItemNode = self.#originItemNodeList[0].cloneNode(true);

            self.#floorNum = floorNum;
            self.#pid = pid;

            self.#expandNum = 0;

            // 读取复制data-v
            let dvlist = [];
            for (let dv in self.#originItemNodeList[0].querySelector(".thread-text").dataset) {
                dvlist.push(`data-v-${dv.slice(1).replace('-', '')}`);
            }
            self.#data_v_a = "data-v-aeeee";
            self.#data_v_b = "data-v-beeee";
            AUTO_CATCH_ERROR(() => {
                self.#data_v_a = dvlist[0];
            });
            AUTO_CATCH_ERROR(() => {
                self.#data_v_b = dvlist[1];
            });

            self.#lzTagHTML = `<svg ${self.#data_v_b}="" class="landlord"><use xlink:href="#icon_landlord"></use></svg>`;

            // 创建新按钮节点
            self.#expandBtnTextNode = document.createElement("span");
            self.#expandBtnTextNode.className = "open-app-text-real";
            self.#expandBtnTextNode.innerHTML = CustomLzlExpandManager.#STR_NEWOPENLZLTEXT;

            // 绑定长按事件
            let clickFunc = SETTINGS_DATA.isClickToExpandLzlPage.value ? self.expandLzl.bind(self) : self.openLzlPage.bind(self);
            let longclickFunc = SETTINGS_DATA.isLongClickToExpandLzlPage.value ? self.expandLzl.bind(self) : self.openLzlPage.bind(self);

            let timeOutEvent = 0;
            const TIME_OUT = 500;
            self.#expandBtnNode.ontouchstart = function () {
                timeOutEvent = setTimeout(function () {
                    timeOutEvent = 0;
                    // 执行长按
                    longclickFunc();
                }, TIME_OUT);
                return false;
            }
            self.#expandBtnNode.ontouchend = function () {
                clearTimeout(timeOutEvent);
                if (timeOutEvent != 0) {
                    // 判断为单击
                    clickFunc();
                }
                return false;
            }
            self.#expandBtnNode.ontouchmove = function () {
                clearTimeout(timeOutEvent);
                timeOutEvent = 0;
            }

            if (IS_DEBUG) {
                // 同步鼠标事件,在PC端进行测试
                self.#expandBtnNode.onmousedown = self.#expandBtnNode.ontouchstart;
                self.#expandBtnNode.onmouseup = self.#expandBtnNode.ontouchend;
                self.#expandBtnNode.onmousemove = self.#expandBtnNode.ontouchmove;
            }

            // 替换新按钮
            self.#expandBtnNode.insertBefore(self.#expandBtnTextNode, self.#expandBtnNode.children[0]);

            // 自动展开剩余评论
            if (SETTINGS_DATA.isAutoExpand.value) {
                DEBUGLOG(self.#floorNum, "AutoExpand");
                self.expandLzl();
            }
        }

        /**
         * 原地展开楼中楼评论
         * @param {Boolean} isTheLast 是否为最后一次展开,避免无限递归
         * @param {function(any):void} [waitFinishFunc] 等待加载完毕,如果发生错误,则会传入相应的错误状态
         * @param {this} self *
         */
        expandLzl(isTheLast = false, waitFinishFunc = undefined, self = this) {
            ASSERT(self.#enable, `尝试展开不存在的评论区,楼层号${self.#floorNum}`);
            DEBUGLOG(self.#floorNum, "expandLzl");

            let mainPage = MainPage.getInstance();
            let someKey = mainPage.someKey;
            let url = `${window.origin}/mg/o/getFloorData?pn=${self.#currentPageNum}&rn=${self.#pageSize}&tid=${someKey.tid}&pid=${self.#pid}`;
            DEBUGLOG(url, "expandLzl");
            let abortFunc = GM.xmlhttpRequest({
                method: "get",
                url: url,
                onload: function (details) {
                    self.#endExpandAnimation();
                    // 爬取解析楼中楼评论数据
                    let floorData = undefined;
                    let subpostlist = undefined;
                    try {
                        floorData = JSON.parse(details.responseText);
                        subpostlist = floorData.data.sub_post_list; // 评论列表
                        if (!subpostlist || subpostlist.length == 0) {
                            throw ("sub_post_list为空");
                        }
                    } catch (e) {
                        // 无法获取楼中楼数据
                        DEBUGLOG(`无法获取楼中楼数据,url:${url}\n错误:${e}`, STR_DEBUG_LABEL_ERROR);
                        self.#showError(true);
                        return;
                    }

                    // 复原颜色
                    self.#showError(false);

                    // 去掉前两个评论
                    if (self.#currentPageNum == 1) {
                        AUTO_CATCH_ERROR(() => {
                            for (let i = self.#originItemNodeList.length - 1; i > -1; i--) {
                                self.#lzlNode.removeChild(self.#originItemNodeList[i]);
                            }
                        });
                    }

                    subpostlist.forEach(function (subpost) { // 遍历每一行评论
                        let contentHTML = ""; // 单行评论的HTML

                        subpost.content.forEach(function (subContent) { // 遍历单行评论的每一个元素
                            let itemHTML = ""; // 元素的HTML
                            switch (subContent.type) {
                                case CustomLzlExpandManager.#LZL_CONTENT_TYPE.EMOJI:
                                    itemHTML = `<img ${self.#data_v_a}="" src="${subContent.src}" alt="${subContent.text}"class="emotion-img">`;
                                    break;
                                case CustomLzlExpandManager.#LZL_CONTENT_TYPE.USERNAME:
                                    if (subContent.uid == mainPage.lzId) {
                                        itemHTML = `<span ${self.#data_v_b}="" class="link username"> ${subContent.text} ${self.#lzTagHTML} </span>`;
                                    } else {
                                        itemHTML = `<span ${self.#data_v_a}="" class="user rich-link-disabled"> ${subContent.text} </span>`;
                                    }
                                    break;
                                case CustomLzlExpandManager.#LZL_CONTENT_TYPE.TEXT:
                                default:
                                    // 如有其他的类型暂时用文本代替
                                    itemHTML = `<span ${self.#data_v_a}="" class="text-content">${subContent.text}</span>`;
                                    break;
                            }
                            contentHTML += itemHTML;
                        })
                        let newItemNode = self.#sampleItemNode.cloneNode(true); // 新的评论行
                        newItemNode.querySelector(".username").innerHTML = `${subpost.author.show_nickname} ${(mainPage.lzId == subpost.author.id) ? self.#lzTagHTML : ""}:`;
                        newItemNode.querySelector(".thread-text").innerHTML = contentHTML;
                        self.#lzlNode.insertBefore(newItemNode, self.#expandBtnNode);
                    });

                    // 展开结束后处理剩余评论
                    let pageinfo = floorData.data.page; // 楼中楼信息,包括楼层数、页面大小、页面数量
                    let total_page = parseInt(pageinfo.total_page); // 总页数
                    if (total_page > self.#currentPageNum) { // 仍有剩余评论未展开
                        self.#currentPageNum++;
                        let total_num = parseInt(pageinfo.total_num);
                        let remaind_num = total_num - self.#pageSize * (self.#currentPageNum - 1);
                        self.#expandBtnNode.children[0].innerHTML = CustomLzlExpandManager.#STR_REMAINDOPENLZLTEXT(remaind_num);
                        if (SETTINGS_DATA.isRemaindAutoExpand.value && SETTINGS_DATA.remaindAutoExpandSize.value > remaind_num) { // 当剩余评论过少时自动展开
                            if (!isTheLast) { // 检查当前是否强制设置为为最后一次展开(避免楼中楼更新时无限递归)
                                self.expandLzl(true);
                            }
                        } else if (SETTINGS_DATA.isAutoExpand.value && self.#expandNum < SETTINGS_DATA.autoExpandNum.value) { // 自动展开剩余评论
                            self.expandLzl();
                        }
                    } else { // 所有评论已展开时隐藏展开按钮
                        self.#destroy.apply(self);
                    }
                    // 成功展开后记录次数
                    self.#expandNum++;
                },
                onerror: function (details) {
                    self.#endExpandAnimation();
                    self.#showError(true);
                    DEBUGLOG(`无法加载评论区,爬取的url为${details.responseURL}`, STR_DEBUG_LABEL_ERROR);
                },
                onabort: onerror,
                ontimeout: onerror,
            });

            // 动画处理
            self.#startExpandAnimation(abortFunc);

        }

        // 另一种打开楼中楼的方法,将页面加载到iframe弹框里
        openLzlPage(self = this) {
            let someKey = MainPage.getInstance().someKey;
            if (!someKey.tid || !someKey.postAuthorId || !someKey.forumId) {
                self.#showError();
                return;
            }
            let newHash = `#/lzlPage?tid=${someKey.tid}&pid=${self.#pid}&floor=${self.#floorNum}&postAuthorId=${someKey.postAuthorId}&forumId=${someKey.forumId}`;
            DEBUGLOG(newHash, "openLzlPage hash");

            LzlPage.getInstance().showAndReload(newHash);
        }

        #showError(isError = true, self = this) {
            if (isError) {
                self.#expandBtnTextNode.classList.add("error");
            } else {
                self.#expandBtnTextNode.classList.remove("error");
            }
        }

        // 开始动画
        #startExpandAnimation(abortFunc, self = this) {
            self.#expandBtnNode.disabled = true;
            self.#expandBtnNode.classList.add("loading");

            self.#expandTimeoutTimer = setTimeout(function () {
                self.#endExpandAnimation();
                self.#showError(true);
                DEBUGLOG(`加载异常,并且超时未处理`, STR_DEBUG_LABEL_ERROR);
                abortFunc();
            }, 5000);
        }

        // 结束动画
        #endExpandAnimation(self = this) {
            self.#expandBtnNode.disabled = false;
            self.#expandBtnNode.classList.remove("loading");

            if (self.#expandTimeoutTimer) {
                clearTimeout(self.#expandTimeoutTimer);
                self.#expandTimeoutTimer = undefined;
            }
        }

        #destroy(self = this) {
            try {
                self.#enable = false;
                self.#expandBtnNode.style.display = "none";
                self.#expandBtnNode.parentNode.removeChild(self.#expandBtnNode);
            } finally {
                //this = null;
            }
        }
    }

    /**
     * 栈
     * @template T 
     * */
    class Stack {
        /**@type {Array<T>} */
        #items;
        constructor() {
            this.#items = [];
        }
        /**
         * @param {T} element
         */
        push(element) {
            this.#items.push(element);
        };

        pop() {
            return this.#items.pop();
        };

        peek() {
            return this.#items[this.#items.length - 1];
        };

        isEmpty() {
            return this.#items.length == 0;
        };

        size() {
            return this.#items.length;
        };

        clear() {
            this.#items = [];
        };
    }

    /**
     * 队列
     * @template T 
     * */
    class Queue {
        /**@type {Array<T>} */
        #queue;
        constructor() {
            this.#queue = [];
        }
        /**@param {T} element*/
        enqueue(element) {
            this.#queue.push(element);
        };
        dequeue() {
            return this.#queue.shift();
        };
        /** @param {number} index*/
        at(index) {
            return this.#queue[index];
        }
        get front() {
            return this.#queue[0];
        };
        get isEmpty() {
            return this.#queue.length === 0;
        };
        get length() {
            return this.#queue.length;
        };
    }

    // 楼中楼弹框的缓存管理
    class LzlPageCacheManager {
        #srcQueue; // 网址队列
        #iframeList; // 对应网址的iframe对象集合
        constructor() {
            this.#srcQueue = new Queue();
            this.#iframeList = new Map();
        }
        appendOrReplace(src, lzlIframeNode) {
            if (this.#iframeList.has(src)) { // 缓存里找到并替换原有页面
                this.#removeOldNode(src);
            } else { // 缓存里找不到对应页面
                this.#srcQueue.enqueue(src);
                if (this.#srcQueue.length > SETTINGS_DATA.lzlCacheSize.value) { // 页面数量过大
                    let oldSrc = this.#srcQueue.dequeue();
                    this.#removeOldNode(oldSrc);
                }
            }

            this.#iframeList.set(src, lzlIframeNode);
            DEBUGLOG(`${src}\n${lzlIframeNode.innerHTML}`, "appendOrReplace");

        }

        get(src) {
            if (this.#iframeList.has(src)) {
                return this.#iframeList.get(src);
            } else {
                return undefined;
            }
        }

        #removeOldNode(oldSrc) {
            let oldIframe = this.#iframeList.get(oldSrc);
            DEBUGLOG(oldSrc, "remove");
            oldIframe.parentNode.removeChild(oldIframe);
            this.#iframeList.delete(oldSrc);
            oldIframe = undefined;
        }

    }

    // 下滑触发隐藏的控制器
    class LzlPageSlideDownController {
        static #DISTANCE_PX_SHOW_SLIDE_BTN = CM2PX(5); // 显示下拉按钮的滑动距离
        static #DISTANCE_PX_CONFIRM_SLIDE_DOWN = CM2PX(10); // 滑动距离大于此值,则隐藏窗口,单位为像素

        /**@type {Window} */
        #iframeWindow;
        /**@type {HTMLIFrameElement} */
        #iframeNode;

        #startTouchY = NaN; // (当数字与NaN比较时,始终为false)
        #onslidedownendFunc;

        #slideDownBtnNode; // 下拉按钮
        get slideDownBtnNode() {
            return this.#slideDownBtnNode;
        }

        constructor(onslidedownendFunc = undefined) {
            let self = this;
            self.#onslidedownendFunc = onslidedownendFunc;

            // 下拉按钮
            self.#slideDownBtnNode = document.createElement("div");
            self.#slideDownBtnNode.className = "lzl-nav-btn slide-down-btn";
            self.#slideDownBtnNode.innerHTML = HTML_SVG_DOWN_BTN;
        }

        /**
         * 
         * @param {HTMLIFrameElement} iframeNode
         */
        setIframe(iframeNode, self = this) {
            self.#iframeNode = iframeNode;
            // @ts-ignore
            self.#iframeWindow = iframeNode.contentWindow;
            self.#iframeWindow.document.addEventListener("touchstart", self.#touchstart.bind(self), false);
            self.#iframeWindow.document.addEventListener("touchmove", self.#touchmove.bind(self), false);
            self.#iframeWindow.document.addEventListener("touchend", self.#touchend.bind(self), false);
            // self.#iframeWindow.document.ontouchstart = self.#touchstart.bind(self);
            // self.#iframeWindow.document.ontouchmove = self.#touchmove.bind(self);
            // self.#iframeWindow.document.ontouchend = self.#touchend.bind(self);
        }

        #triggerSlideAbort(self = this) { // 整个滑动动作结束后的收尾工作
            self.#startTouchY = NaN;

            self.#slideDownBtnNode.style.marginTop = 0;
            self.#slideDownBtnNode.style.opacity = 0;
            self.#slideDownBtnNode.classList.remove("confirm");
        }

        /** @param {TouchEvent} event*/
        #touchstart(event, self = this) {
            DEBUGLOG(self.#iframeWindow.scrollY, "touchstart");
            self.#iframeNode.focus();
            self.#iframeWindow.focus();
            if (self.#iframeWindow.scrollY < 5) { // 在顶部继续下滑
                self.#startTouchY = event.changedTouches[0].screenY;
            }
            return STOPPROPAGATION(event);
        }

        /** @param {TouchEvent} event*/
        #touchmove(event, self = this) {
            event.preventDefault();
            let touchY = event.changedTouches[0].screenY;
            DEBUGLOG(touchY, "touchmove");
            if (!self.#startTouchY && self.#iframeWindow.scrollY < 5) { // 开始在顶部继续下滑
                self.#startTouchY = touchY;
            }
            if (self.#startTouchY) { // 在顶部继续下滑
                let distance = touchY - self.#startTouchY; // 滑动距离
                DEBUGLOG(distance, "distance")
                if (distance > LzlPageSlideDownController.#DISTANCE_PX_SHOW_SLIDE_BTN) { // 显示按钮
                    self.#slideDownBtnNode.style.opacity = 1;
                    if (distance > LzlPageSlideDownController.#DISTANCE_PX_CONFIRM_SLIDE_DOWN) { // 抵达预定位置,切换按钮确认状态
                        self.#slideDownBtnNode.classList.add("confirm");
                        self.#slideDownBtnNode.style.marginTop = `${LzlPageSlideDownController.#DISTANCE_PX_CONFIRM_SLIDE_DOWN}px`;
                    } else { // 未达确认状态
                        self.#slideDownBtnNode.classList.remove("confirm");
                        self.#slideDownBtnNode.style.marginTop = `${distance}px`;
                    }
                } else { // 逐渐显示按钮
                    let opacity = distance / LzlPageSlideDownController.#DISTANCE_PX_SHOW_SLIDE_BTN;
                    self.#slideDownBtnNode.style.opacity = opacity;
                }
            } else { // 正常滚动
            }
            return STOPPROPAGATION(event);
        }

        /** @param {TouchEvent} event*/
        #touchend(event, self = this) {
            try {
                DEBUGLOG("touchend", "touchend");
                if (self.#slideDownBtnNode.classList.contains("confirm")) { // 确认下滑,执行隐藏命令
                    self.#onslidedownendFunc();
                } else { // 没有确认,恢复按钮状态,并且弹框不执行任何动作

                }
            } finally {
                self.#triggerSlideAbort();
                return STOPPROPAGATION(event);
            }
        }
    }

    class LzlPage extends VoeocDialog {
        /**@type {LzlPageCacheManager} */
        #lzlPageCacheManager;
        /**@type {LzlPageSlideDownController} */
        #lzlPageSlideDownController;
        // HTML节点
        #lzlPageNode;
        /**@type {HTMLIFrameElement | undefined} */
        #lzlPageIframeNode;
        #lzlPageBackgroundNode;
        #reloadBtnNode; // 刷新按钮
        #closeBtnNode; // 关闭按钮

        /**@returns {LzlPage} */
        static getInstance() {
            throw new Error("请使用SingleInstanceManager初始化");
        }

        constructor() {
            SingleInstanceManager.CHECK_SINGLE_INSTANCE(LzlPage);
            super();
        }

        init(self = this) {
            ASSERT(!document.getElementById(STR_ID_LZLPAGE), "已存在楼中楼弹框id,请检查代码");

            self.#lzlPageCacheManager = new LzlPageCacheManager();
            self.#lzlPageSlideDownController = new LzlPageSlideDownController(this.close.bind(this));

            // 楼中楼展示页
            self.#lzlPageNode = document.createElement("div");
            self.#lzlPageNode.id = STR_ID_LZLPAGE;
            self.#lzlPageNode.ontouchmove = STOPPROPAGATION;
            self.#lzlPageNode.onscroll = STOPPROPAGATION;

            super.setState(HISTORY_STATE.LZLPAGE, self.#lzlPageNode);

            // 显隐动画结束事件
            self.#lzlPageNode.addEventListener("transitionend", function () {
                if (self.#lzlPageNode.classList.contains("show")) { // 显示完毕

                } else { // 隐藏完毕

                }
            }, false);

            // 刷新按钮
            self.#reloadBtnNode = document.createElement("div");
            self.#reloadBtnNode.className = "lzl-nav-btn lzl-reload-btn";
            self.#reloadBtnNode.innerHTML = HTML_SVG_RELOAD_BTN;
            self.#reloadBtnNode.reloadTimeOut = undefined;
            self.#reloadBtnNode.startLoading = function () {
                self.#lzlPageNode.classList.add("loading");
                clearTimeout(self.#reloadBtnNode.reloadTimeOut);
                self.#reloadBtnNode.reloadTimeOut = setTimeout(function () {
                    self.#lzlPageNode.classList.remove("loading");
                    self.#lzlPageNode.classList.add("error");
                }, 3000)
            }
            self.#reloadBtnNode.finishLoading = function () {
                self.#lzlPageNode.classList.remove("loading");
                clearTimeout(self.#reloadBtnNode.reloadTimeOut);
                self.#reloadBtnNode.reloadTimeOut = undefined;
                self.#lzlPageNode.classList.remove("error");
            }

            // 关闭按钮
            self.#closeBtnNode = document.createElement("div");
            self.#closeBtnNode.className = "lzl-nav-btn";
            self.#closeBtnNode.style.cssText = "margin-right:.1rem;";
            self.#closeBtnNode.innerHTML = HTML_SVG_CLOSE_BTN;

            // 用于加载实际页面的iframe
            self.#lzlPageIframeNode = undefined;

            // 背景板
            self.#lzlPageBackgroundNode = document.createElement("div");
            self.#lzlPageBackgroundNode.id = STR_ID_LZLPAGEBACKGROUND;

            // 用于拦截滚动链
            let lzlPageScrollContentNode = document.createElement("div");
            lzlPageScrollContentNode.style.cssText = "height:101%;";

            // 点击事件
            function close() {
                self.close();
            }
            self.#lzlPageBackgroundNode.onclick = close;
            lzlPageScrollContentNode.onclick = close;
            self.#closeBtnNode.onclick = close;
            self.#reloadBtnNode.onclick = function () {
                self.reload();
            };

            // 添加元素到页面
            self.#lzlPageNode.appendChild(self.#lzlPageBackgroundNode);
            self.#lzlPageNode.appendChild(lzlPageScrollContentNode);
            self.#lzlPageNode.appendChild(self.#reloadBtnNode);
            self.#lzlPageNode.appendChild(self.#lzlPageSlideDownController.slideDownBtnNode);
            document.body.insertBefore(self.#lzlPageNode, document.body.children[0]);
        }

        static generateNewLzlPageIframeNode(src = "") {
            // 楼中楼iframe加载器
            let newLzlPageIframeNode = document.createElement("iframe");
            newLzlPageIframeNode.className = `${STR_CLASSNAME_LZLPAGEIFRAME}`;
            newLzlPageIframeNode.setAttribute("src", src);
            newLzlPageIframeNode.setAttribute("frameborder", "0");
            return newLzlPageIframeNode;
        }

        #lastLoadHash;
        #reloadNum;
        async reload(hash, isBanReload = false, self = this) {
            if (isBanReload) { // 禁止重复加载同一页面
                try {
                    if (self.#lzlPageIframeNode?.contentWindow?.location.hash == hash) {
                        return;
                    }
                } catch (e) {
                    DEBUGLOG(e, STR_DEBUG_LABEL_ERROR);
                }
            }

            // 递归(刷新)次数限制
            // if (self.#reloadNum > 2) {
            //     return;
            // }

            // 检查重复
            if (!hash) {
                hash = self.#lastLoadHash;
                self.#reloadNum++;
            } else {
                if (hash == self.#lastLoadHash) { // 传入的hash重复了
                    self.#reloadNum++;
                } else {
                    self.#reloadNum = 0;
                    self.#lastLoadHash = hash;
                }
            }

            // 生成新的页面
            let newLzlPageIframeNode = LzlPage.generateNewLzlPageIframeNode(hash);
            self.#lzlPageCacheManager.appendOrReplace(hash, newLzlPageIframeNode);
            self.#lzlPageIframeNode = newLzlPageIframeNode;
            self.#lzlPageNode.insertBefore(self.#lzlPageIframeNode, self.#lzlPageBackgroundNode);
            if (self.#reloadNum > 0) { // 原地刷新
                self.#lzlPageIframeNode.classList.add("show");
            } else {
                setTimeout(() => {
                    self.#lzlPageIframeNode?.classList.add("show");
                }, 0);
            }

            // 获取载入结果
            self.#reloadBtnNode.startLoading();
            let navbar = undefined;
            try {
                navbar = await WAIT_ELEMENT_LOADED_ASYNC(".nav-bar-top", 5,
                    function (selector) { // 搜索函数
                        if (!self.#lzlPageIframeNode || !self.#lzlPageIframeNode.contentDocument) {
                            return undefined;
                        }
                        return self.#lzlPageIframeNode.contentDocument.querySelector(selector);
                    });
            } catch (e) { // 查找失败,说明没有加载成功
                DEBUGLOG(e, STR_DEBUG_LABEL_ERROR);
                //self.reload(); // 递归刷新
                return;
            }

            // 处理载入
            let iframeWindow = self.#lzlPageIframeNode.contentWindow;
            try {
                if (!iframeWindow) {
                    throw new Error(`楼中楼加载错误。iframeWindow=null`);
                }
                // 拦截滚动链
                /**@type {HTMLElement} */(iframeWindow.document.getElementsByTagName("HTML")[0]).style.overscrollBehavior = "contain";
                // 下滑功能
                WAIT_DOCUMENT_READY(function () {
                    if (self.#lzlPageIframeNode) {
                        // 添加下滑隐藏功能
                        self.#lzlPageSlideDownController.setIframe(self.#lzlPageIframeNode);
                        // 获取焦点
                        self.#lzlPageIframeNode.focus();
                        // 尝试屏蔽上层滚动
                        self.#lzlPageIframeNode.contentWindow?.onscroll ?? STOPPROPAGATION;
                    }

                }, iframeWindow.document)
                // 隐藏多余按钮
                INSERT_CSS(STR_CSS_REMOVE, iframeWindow.document);
                let backBtnNode = navbar.querySelector(".logo-wrapper");
                let openAppBtnNode = navbar.querySelector(".more-btn-desc");
                backBtnNode.disabled = true;
                backBtnNode.style.visibility = "hidden";
                openAppBtnNode.style.display = "none";
                // 添加新的关闭按钮
                let newCloseBtnNode = self.#closeBtnNode.cloneNode(true);
                newCloseBtnNode.onclick = self.#closeBtnNode.onclick;
                navbar.replaceChild(newCloseBtnNode, openAppBtnNode);

            } catch (e) {
                DEBUGLOG(e, STR_DEBUG_LABEL_ERROR);
            } finally {
                // 结束动画。若无法执行到此处,则会自动超时显示变成错误状态
                self.#reloadBtnNode.finishLoading();
            }
        }

        showAndReload(hash, self = this) {

            if (self.#lzlPageIframeNode) { // 隐藏前一页面
                self.#lzlPageIframeNode.classList.remove("show");
            }
            self.#lzlPageIframeNode = self.#lzlPageCacheManager.get(hash);
            if (self.#lzlPageIframeNode) { // 成功加载了缓存的内容
                DEBUGLOG(`显示缓存页${hash}`, "cache");
                self.#lzlPageIframeNode.classList.add("show");
            } else {
                // 没有缓存,直接刷新
                DEBUGLOG(`加载新页面${hash}`, "cache");
                self.reload(hash, true);
            }

            // 设置评论页为显示
            self.open();
        }
    }

    class MainPage {
        /**@type {HTMLElement | null}  一楼*/
        tieNode;
        /**@type {HTMLElement | null}  吧名*/
        tiebaNameNode;
        lzId = ""; // 楼主id

        currentHash = window.location.hash; // 存储当前页的Hash,页面变动的依据

        /**@type {number} 存储当前滚动位置 */
        currentScrollYPos;

        floorDataList = {} // 存储所有楼层数据以便搜索,索引为楼层数字符串,值为抓取的json。主要信息为pid,获取路径floorDataList[floor].id
        someKey = { // 一些用于网络请求的关键字,页面加载或变动时自自动更新
            host: "tieba.baidu.com",
            tid: "",
            postAuthorId: "",
            forumId: "",
        }

        /**@returns {MainPage} */
        static getInstance() {
            throw new Error("请使用SingleInstanceManager初始化");
        }

        constructor() {
            SingleInstanceManager.CHECK_SINGLE_INSTANCE(MainPage);
        }

        init(self = this) {
            AUTO_CATCH_ERROR(() => {
                // 监听楼层加载的网络事件
                // @ts-ignore
                let oldXHR = GM.unsafeWindow.XMLHttpRequest; GM.unsafeWindow.XMLHttpRequest = function () {
                    let realXHR = new oldXHR();
                    realXHR.addEventListener('readystatechange', function () {
                        DEBUGLOG(realXHR.responseURL, "realXHR.responseURL");
                        if (VOEOC_REG.PBDATA.test(realXHR.responseURL) && realXHR.response != "") {
                            self.parsePbData(realXHR.response, realXHR.responseURL);
                        }
                    }, false);
                    return realXHR;
                }

                // 通过监听页面滚动变化获取页面变动情况
                GM.unsafeWindow.onscroll = function () {
                    self.checkUrlHashChange();
                    if (getPageType() == PAGE_TYPE.POSTPAGE) { // 只会记录评论页的滚动位置
                        if (window.pageYOffset != 0) {
                            // 记录当前滚动位置
                            self.currentScrollYPos = window.pageYOffset;
                            //DEBUGLOG(scrollPos, "scrollPos");
                        }
                    }
                }

                // 页面变动时触发检查
                GM.unsafeWindow.onhashchange = function (e) {
                    self.checkUrlHashChange();
                }
            });

            REGISTER_MENUCOMMAND();

            // 初始化楼中楼弹出框
            LzlPage.getInstance().init();
            // 初始化设置窗口弹框
            SettingDialog.getInstance().init();
            // 初始化弹窗管理器
            HistoryStateManager.getInstance().init();

            WAIT_ELEMENT_LOADED("div.nav-bar-v2-fixed:nth-child(1)", (navbarfixed) => {
                self.tiebaNameNode = document.querySelector(".forum-block"); // 获取吧名
                self.tieNode = document.querySelector(".main-thread-content"); // 获取楼主发帖内容

                /**@type {HTMLElement | null} */
                let postbtn = document.querySelector(".post-page-entry-btn"); // 展开评论页的按钮

                // 点击展开按钮
                if (postbtn) {
                    postbtn.click();
                    // 手动触发页面刷新检测
                    self.checkUrlHashChange();
                } else { // 展开按钮不存在,可能是楼层太少了
                    DEBUGLOG(postbtn, "postbtn");
                    // 页面开始加载时监听会大概率失效,所以这里只能主动触发抓取楼层信息的请求,发起后交给监听程序
                    let pageId = MATCH_REG(RegExp(`${window.origin}/p/([0-9]*)?#?/?`, 'i'), window.location.href);
                    let url = `${window.origin}/mg/p/getPbData?kz=${pageId}&obj_param2=firefox&format=json&eqid=&refer=&pn=1&rn=5`;
                    DEBUGLOG(url, "url");
                    GM.xmlhttpRequest({
                        method: "get",
                        url: url,
                        onload: function (details) {
                            DEBUGLOG(details.responseText, "xmlhttpRequest onload");
                            // 收集数据
                            self.parsePbData(details.responseText, url);
                            // 使用获取的新数据,强制手动修改页面
                            self.checkUrlHashChange(true);
                        },
                        onerror: function (details) {
                            DEBUGLOG(`获取主页数据失败,url:${details.responseURL}`, STR_DEBUG_LABEL_ERROR);
                        },
                    });
                }
            })
        }

        /**
         * 开始监听楼层变化
         * @param {HTMLElement} floorParentNode
         */
        listenFloorChange(floorParentNode, self = this) {
            /**
             * 当有新楼层加载时调用
             * @param {HTMLElement} floorNode
             */
            function onNewFloorAdded(floorNode) {
                if (floorNode.classList.contains(STR_VOEOCMARK)) { // 已被打上标记
                    return;
                }

                floorNode.classList.add(STR_VOEOCMARK); // 手动标记,避免重复操作

                AUTO_CATCH_ERROR(() => {
                    /**@type {HTMLElement | null} */
                    let expandBtnNode = floorNode.querySelector(".open-app-guide"); // 楼中楼展开按钮
                    if (expandBtnNode) {
                        let floorinfoNode = floorNode.querySelector(".floor-info"); // 楼层数元素
                        if (!floorinfoNode) {
                            throw new Error("楼层数节点不存在");
                        }
                        let floorNum = MATCH_REG(RegExp(`第([0-9]+)楼`, 'i'), floorinfoNode.innerHTML); // 楼层数
                        if (!floorNum) {
                            throw new Error("无法获取楼层数");
                        }
                        let floorPid = self.floorDataList[floorNum].id; // 楼层id
                        let newCustomLzlExpandManager = new CustomLzlExpandManager(floorNode, expandBtnNode, floorNum, floorPid);
                    }
                });
            };

            // 使用新按钮刷新楼层
            function searchAndUpdatePostPage() {
                // 遍历所有新加的楼层元素
                /**@type {NodeListOf<HTMLElement>} */
                let floorNodeList = floorParentNode.querySelectorAll(`div.post-item:not(.${STR_VOEOCMARK})`);
                floorNodeList.forEach(onNewFloorAdded);
            }

            // 注册楼层元素添加事件
            let observer = new MutationObserver(function (_mutationList) {
                searchAndUpdatePostPage();
            });
            observer.observe(floorParentNode, {
                attributes: false,
                childList: true,
                characterData: false,
                subtree: false,
            });
            searchAndUpdatePostPage();
        }

        /**
         * 检测页面Hash变化,变化时将根据类型执行页面修改
         * @param {boolean} force 为true时,无论检测到Hash是否变化均执行后续任务
         * @param {this} self
         * @returns
         */
        checkUrlHashChange(force = false, self = this) {
            if (self.currentHash != window.location.hash) {
                self.currentHash = window.location.hash;
            } else if (!force) { // 不强制执行修改的话,在hash没变时将直接退出函数
                return false;
            }

            // 添加设置按钮
            WAIT_ELEMENT_LOADED(".nav-bar-top", (navbar) => {
                AUTO_CATCH_ERROR(() => {
                    const id = "VOEOC-ID-SETTINGBTNNODE";
                    if (document.getElementById(id)) {
                        return;
                    }
                    let settingBtnNode = document.createElement("div");
                    settingBtnNode.id = id;
                    settingBtnNode.className = "lzl-nav-btn";
                    settingBtnNode.style.cssText = "position:fixed;margin: 0.1rem;";
                    settingBtnNode.innerHTML = HTML_SVG_SETTING_BTN;
                    settingBtnNode.onclick = SettingDialog.OPEN_DIALOG;
                    navbar.appendChild(settingBtnNode);
                });
            })

            let pageType = getPageType();
            if (pageType == PAGE_TYPE.POSTPAGE) { // 页面变动为评论页
                // 收集url数据
                let tid = GET_URL_ATTR(self.currentHash, "tid");
                let postAuthorId = GET_URL_ATTR(self.currentHash, "postAuthorId");
                let forumId = GET_URL_ATTR(self.currentHash, "forumId");

                self.someKey.host = window.location.hostname;
                if (tid) { self.someKey.tid = tid; }
                if (postAuthorId) { self.someKey.postAuthorId = postAuthorId; }
                if (forumId) { self.someKey.forumId = forumId; }

                // 当页面变动时,刷新展开楼层的按钮
                WAIT_ELEMENT_LOADED(".post-page-list", (postpagelist) => { // 等待页面加载完成
                    self.listenFloorChange(postpagelist);
                }, 10);

                // 恢复一楼显示
                self.restorePostPage();

                // 页面切换后恢复滚动位置
                MainPage.scrollTo(self.currentScrollYPos);
            } else if (pageType == PAGE_TYPE.MAINPAGE) { // 页面变动为主页
                if (self.tieNode) {
                    // 当页面变动时,刷新展开楼层的按钮
                    WAIT_ELEMENT_LOADED(".pb-page-wrapper", (pbpageNode) => { // 等待页面加载完成
                        self.listenFloorChange(pbpageNode);
                    }, 10);

                    // 将剪切走的一楼复制回来
                    WAIT_ELEMENT_LOADED("#replySwitch", (splitlineNode) => { // 等待页面加载完成
                        AUTO_CATCH_ERROR(() => {
                            if (!self.tieNode) {
                                throw new Error("1楼不存在");
                            }
                            if (!splitlineNode.parentNode) {
                                throw new Error("复原1楼时页面加载失败");
                            }
                            splitlineNode.parentNode.insertBefore(self.tieNode, splitlineNode);

                        });
                    }, 10);
                    MainPage.scrollTo(0);
                }
            }
            return true;
        }

        // 滚动到指定y坐标(页面刷新后,如果当前楼层数比较大,只能滚动到贴末尾的最大加载位置)
        static scrollTo(yPos, documentNode = document) {
            WAIT_ELEMENT_LOADED(".post-page", (_) => { // 等待页面加载完成
                documentNode.documentElement.scrollTop = yPos;
                // 在一定时间内维持滚动位置
                setTimeout(function () {
                    documentNode.documentElement.scrollTop = yPos;
                    DEBUGLOG(yPos, "scrollTo");
                }, 200);
            });
        }

        // 修改评论页,加入缺失的1楼和贴吧名等
        restorePostPage(self = this) {
            DEBUGLOG("restore")
            WAIT_ELEMENT_LOADED(".text", (titletextNode) => { // 等待标题位置加载
                // 显示贴吧名
                AUTO_CATCH_ERROR(() => {
                    if (!self.tiebaNameNode || !titletextNode.parentNode) {
                        throw new Error("贴吧名无法加载");
                    }
                    /**@type {HTMLElement} */
                    // @ts-ignore
                    let tiebaNameCloneNode = self.tiebaNameNode.cloneNode(true);
                    titletextNode.parentNode.replaceChild(tiebaNameCloneNode, titletextNode);
                    // 关联点击贴吧名的事件
                    tiebaNameCloneNode.onclick = self.tiebaNameNode.click.bind(self.tiebaNameNode);

                });

                // 显示楼主发帖层
                AUTO_CATCH_ERROR(() => {
                    if (!self.tieNode) {
                        throw new Error("1楼内容为空!");
                    }
                    // 复原样式丢失
                    self.tieNode.style.cssText = `margin-left:0.12rem;margin-right:0.12rem;margin-bottom:0.25rem;`


                    /** @type {HTMLElement | null} 尝试找回楼主丢失的头像*/
                    let lzavatarNode = self.tieNode.querySelector(".avatar");
                    if (lzavatarNode) {
                        lzavatarNode.style.backgroundImage = `url("${lzavatarNode.getAttribute("data-src")}")`
                    } else {
                        DEBUGLOG("找不到楼主头像", STR_DEBUG_LABEL_ERROR);
                    }

                    /** @type {HTMLElement | null} 尝试复原一楼的文字内容的字体样式*/
                    let textContentNode = self.tieNode.querySelector(".thread-text");
                    if (textContentNode) {
                        textContentNode.style.cssText = `margin-top:0.18rem;font-size:0.16rem;line-height:0.28rem;`
                    } else {
                        DEBUGLOG("1楼不存在", STR_DEBUG_LABEL_ERROR);
                    }

                    /** @type {HTMLElement | null} 标题下方的分割,用于插入一楼*/
                    let replySwitchNode = document.querySelector("#replySwitch"); //
                    if (replySwitchNode && replySwitchNode.parentNode) {
                        replySwitchNode.parentNode.insertBefore(self.tieNode, replySwitchNode);
                    } else {
                        DEBUGLOG("1楼插入失败", STR_DEBUG_LABEL_ERROR);
                    }

                });

                // 尝试复原标题样式
                AUTO_CATCH_ERROR(() => {
                    /**@type {HTMLElement | null} */
                    let threadtitleNode = document.querySelector(".thread-title");
                    let isTopTitle = true;
                    if (!threadtitleNode) {
                        threadtitleNode = document.querySelector(".bottom-thread-title");
                        isTopTitle = false;
                    }
                    if (!threadtitleNode) {
                        throw new Error("标题为空!");
                    }
                    threadtitleNode.style.cssText = `margin-bottom: 0.13rem;font-size:0.22rem;font-weight:700;line-height:0.33rem;`

                    /**@todo 置顶标题显示 */
                    if (false && isTopTitle) {
                        // let threadtitleCloneNode = threadtitleNode.cloneNode(true)
                        // threadtitleNode.style.visibility = "hidden";
                        // threadtitleCloneNode.style.cssText += `position: fixed !important;z-index: 99 !important;opacity: 0.8 !important;background-color: #FFFFFF !important;`
                        // threadtitleNode.parentNode.insertBefore(threadtitleCloneNode, threadtitleNode)
                    }
                });
            })
        }

        // 解析传过来的PBDATA的json
        /**
         * @param {any} responseText
         * @param {string} responseURL
         */
        parsePbData(responseText, responseURL, self = this) {
            AUTO_CATCH_ERROR(() => {
                let data = undefined;
                if (typeof responseText == "object") {
                    data = responseText.data;
                } else {
                    data = JSON.parse(responseText).data;
                }
                ASSERT(data, "无法获取PBDATA");

                // 获取内部参数
                AUTO_CATCH_ERROR(() => {
                    self.someKey.tid = data.forum.id;
                }, "该json并暂无tid信息");

                AUTO_CATCH_ERROR(() => {
                    let post_list = data.post_list;
                    if (!post_list) {
                        throw new Error(`该PBDATA没有post_list数据, ${data}`);
                    }
                    // 获取楼主id
                    AUTO_CATCH_ERROR(() => {
                        if (GET_URL_ATTR(responseURL, "pn") == "1") { // 楼主id存在1楼数据中
                            self.lzId = post_list[0].author.id;
                            DEBUGLOG(self.lzId, "lzId");
                        }
                    }, "无法获取楼主id");

                    // 获取楼层信息
                    for (let i = 0; i < post_list.length; i++) {
                        let d = post_list[i];
                        self.floorDataList[d.floor] = d;
                    }
                });
            });
        }
    }

    (function main() {
        // 首次进入,对于不同的页面采取不同的行为
        switch (getPageType()) {
            case PAGE_TYPE.MAINPAGE: // 首次进入帖子主页
                INSERT_CSS(STR_CSS_REMOVE + STR_CSS_MAINPAGE);

                SingleInstanceManager.ORDER_SINGLE(LzlPage);
                SingleInstanceManager.ORDER_SINGLE(MainPage);
                SingleInstanceManager.ORDER_SINGLE(SettingDialog);
                SingleInstanceManager.ORDER_SINGLE(HistoryStateManager);

                MainPage.getInstance().init();

                // @ts-ignore
                GM.unsafeWindow.setDEBUGLOG = function (/** @type {boolean} */ enable) {
                    GM.setValue(STR_GMKEY_IS_DEBUG, enable);
                    IS_DEBUG = enable;
                    console.log(`DEBUGLOG is ${IS_DEBUG ? "enabled" : "disabled"}`);
                }
                break;
            case PAGE_TYPE.POSTPAGE:
                DEBUGLOG("评论页未前置处理", STR_DEBUG_LABEL_ERROR);
                break;
            case PAGE_TYPE.LZLPAGE:
                DEBUGLOG("已切换到楼中楼页", STR_DEBUG_LABEL_ERROR);
                break;
            default:
                DEBUGLOG("未知网址", STR_DEBUG_LABEL_ERROR);
                break;
        }

    })()
})();