pk Web Data Optimizer

Utility for optimizing web data display.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

Advertisement:

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

Advertisement:

// ==UserScript==
// @name         pk Web Data Optimizer
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Utility for optimizing web data display.
// @author       bbk
// @match        https://parks2.bandainamco-am.co.jp/member_mypage.html*
// @match        https://parks2.bandainamco-am.co.jp/admission_use_ticket.html*
// @match        https://parks2.bandainamco-am.co.jp/member_history.html*
// @match        https://parks2.bandainamco-am.co.jp/member_regist.html*
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ==================== 配置区域 ====================

    // 默认替换规则:标签 -> 替换值
    const DEFAULT_REPLACEMENTS = {
        '氏名(漢字)': '山田 太郎',
        '氏名(カナ)': 'ヤマダ タロウ',
        '生年月日': '1990/01/01',
        '性別': '男性',
        '郵便番号': '100-0001',
        '都道府県': '東京都',
        '市区町村': '千代田区千代田',
        '丁目・番地': '1-1-1',
        '電話番号': '09012345678'
    };

    // 需要替换的标签列表(按顺序排列)
    const TARGET_LABELS = [
        '氏名(漢字)',
        '氏名(カナ)',
        '生年月日',
        '性別',
        '郵便番号',
        '都道府県',
        '市区町村',
        '丁目・番地',
        '電話番号'
    ];

    // 性别选项
    const GENDER_OPTIONS = [
        '男性',
        '女性',
        'あてはまらない',
        '回答しない/非表示'
    ];

    // localStorage 键名
    const STORAGE_KEY = 'personal_info_replacements';

    // ==================== 初始防闪烁处理 ====================
    // 核心思想:在元素被替换前保持不可见,替换后通过 data-replaced 属性显示
    (function injectHidingStyle() {
        const style = document.createElement('style');
        style.id = 'hide-member-info-initial';
        style.textContent = `
            /* 初始隐藏目标元素 */
            .block-mypage-member-info-value:not([data-replaced="true"]),
            .block-mypage-coupon-list-item-code-value:not([data-replaced="true"]),
            .block-mypage-history-block-detail-contents-block-content:not([data-replaced="true"]),
            .form-table td .form-input:not([data-replaced="true"]),
            .form-table td .form-input-label:not([data-replaced="true"]),
            .form-table td .text:not([data-replaced="true"]) { 
                opacity: 0 !important; 
            }
            /* 替换后显示,带一点淡入效果 */
            .block-mypage-member-info-value[data-replaced="true"],
            .block-mypage-coupon-list-item-code-value[data-replaced="true"],
            .block-mypage-history-block-detail-contents-block-content[data-replaced="true"],
            .form-table td .form-input[data-replaced="true"],
            .form-table td .form-input-label[data-replaced="true"],
            .form-table td .text[data-replaced="true"] { 
                opacity: 1 !important;
                transition: opacity 0.2s ease-in-out; 
            }
        `;
        if (document.documentElement) {
            document.documentElement.appendChild(style);
        } else {
            const observer = new MutationObserver(() => {
                if (document.documentElement) {
                    document.documentElement.appendChild(style);
                    observer.disconnect();
                }
            });
            observer.observe(document, { childList: true, subtree: true });
        }
    })();

    // ==================== 数据管理 ====================

    /**
     * 从 localStorage 加载替换规则
     */
    function loadReplacements() {
        try {
            const stored = localStorage.getItem(STORAGE_KEY);
            if (stored) {
                return JSON.parse(stored);
            }
        } catch (e) {
            console.error('[信息替换] 读取 localStorage 失败:', e);
        }
        return { ...DEFAULT_REPLACEMENTS };
    }

    /**
     * 保存替换规则到 localStorage
     */
    function saveReplacements(replacements) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(replacements));
            console.log('[信息替换] 已保存到 localStorage');
            return true;
        } catch (e) {
            console.error('[信息替换] 保存到 localStorage 失败:', e);
            return false;
        }
    }

    /**
     * 重置为页面当前显示的数据
     */
    function resetReplacements() {
        const pageData = extractCurrentDataFromPage();
        // 如果页面没有数据(比如不在个人信息页),则使用默认配置
        const newData = Object.keys(pageData).length > 0 ? pageData : { ...DEFAULT_REPLACEMENTS };
        saveReplacements(newData);
        console.log('[信息替换] 已根据页面数据重置初始值');
        return newData;
    }

    /**
     * 从页面提取当前显示的数据
     */
    function extractCurrentDataFromPage() {
        const extracted = {};
        
        // --- 1. 从个人信息页提取 ---
        const dts = document.querySelectorAll('dt.block-mypage-member-info-label');
        dts.forEach(dt => {
            const labelText = dt.textContent.trim();
            if (TARGET_LABELS.includes(labelText)) {
                const dd = dt.nextElementSibling;
                if (dd && dd.classList.contains('block-mypage-member-info-value')) {
                    // 优先从已保存的原始值属性中提取,否则提取当前文本
                    if (dd.hasAttribute('data-original-value')) {
                        extracted[labelText] = dd.getAttribute('data-original-value');
                    } else if (labelText === '性別') {
                        const span = dd.querySelector('span');
                        extracted[labelText] = span ? span.textContent.trim() : dd.textContent.trim();
                    } else {
                        extracted[labelText] = dd.textContent.trim();
                    }
                }
            }
        });

        // --- 2. 从入场券详情页提取姓名 (如果个人信息页没提取到) ---
        if (!extracted['氏名(漢字)']) {
            const ticketNameElem = document.querySelector('.block-mypage-coupon-list-item-code-value');
            if (ticketNameElem) {
                extracted['氏名(漢字)'] = ticketNameElem.hasAttribute('data-original-value') 
                    ? ticketNameElem.getAttribute('data-original-value') 
                    : ticketNameElem.textContent.trim();
            }
        }

        // --- 3. 从订单历史详情页提取姓名/地址/电话 ---
        const historyBlocks = document.querySelectorAll('dl.block-mypage-history-block-detail-contents-block');
        historyBlocks.forEach(block => {
            const dt = block.querySelector('.block-mypage-history-block-detail-contents-block-label');
            const dd = block.querySelector('.block-mypage-history-block-detail-contents-block-content');
            if (!dt || !dd) return;

            const labelText = dt.textContent.trim();
            if (labelText === 'ご依頼主' && !extracted['氏名(漢字)']) {
                extracted['氏名(漢字)'] = dd.hasAttribute('data-original-value')
                    ? dd.getAttribute('data-original-value')
                    : dd.textContent.trim();
            }

            if (labelText === 'お届け先') {
                const rawValue = dd.hasAttribute('data-original-value')
                    ? dd.getAttribute('data-original-value')
                    : getMultilineText(dd);
                const parsed = parseHistoryRecipientText(rawValue);
                Object.keys(parsed).forEach(key => {
                    if (!extracted[key] && parsed[key]) {
                        extracted[key] = parsed[key];
                    }
                });
            }
        });

        // --- 4. 从会员信息编辑页提取 ---
        const formTable = document.querySelector('.form-table');
        if (formTable) {
            const rows = formTable.querySelectorAll('tr');
            rows.forEach(row => {
                const labelElem = row.querySelector('.form-title-label');
                if (!labelElem) return;
                const labelText = labelElem.textContent.trim();
                
                if (labelText === '氏名(漢字)') {
                    const lName = document.getElementById('L_NAME')?.value || '';
                    const fName = document.getElementById('F_NAME')?.value || '';
                    extracted[labelText] = `${lName} ${fName}`.trim();
                } else if (labelText === '氏名(カナ)') {
                    const lKana = document.getElementById('L_KANA')?.value || '';
                    const fKana = document.getElementById('F_KANA')?.value || '';
                    extracted[labelText] = `${lKana} ${fKana}`.trim();
                } else if (labelText === '郵便番号') {
                    extracted[labelText] = document.getElementById('ZIP')?.value || '';
                } else if (labelText === '都道府県') {
                    extracted[labelText] = document.getElementById('ADDR1')?.value || '';
                } else if (labelText === '市区町村') {
                    extracted[labelText] = document.getElementById('ADDR2')?.value || '';
                } else if (labelText === '丁目・番地') {
                    extracted[labelText] = document.getElementById('MEMBER.FREE_ITEM16')?.value || '';
                } else if (labelText === '携帯電話番号(SMS認証)') {
                    extracted['電話番号'] = document.getElementById('TEL')?.value || '';
                } else if (labelText === '生年月日') {
                    const birthLabel = row.querySelector('.form-input-label');
                    if (birthLabel) {
                        extracted[labelText] = birthLabel.textContent.trim();
                    }
                } else if (labelText === '性別') {
                    const genderDiv = row.querySelector('.form-input');
                    if (genderDiv) {
                        extracted[labelText] = genderDiv.textContent.trim();
                    }
                }
            });
        }

        return extracted;
    }

    // ==================== 核心替换逻辑 ====================

    /**
     * 针对你提供的 HTML 结构,精确替换会员信息
     */
    function replaceMemberInfo(replacements) {
        // --- 1. 处理个人信息页 (dt/dd 结构) ---
        const dts = document.querySelectorAll('dt.block-mypage-member-info-label');
        dts.forEach(dt => {
            const labelText = dt.textContent.trim();

            // 为“氏名(漢字)”添加双击打开设置面板的功能
            if (labelText === '氏名(漢字)') {
                setupDblClick(dt);
            }

            // 如果是我们需要替换的标签
            if (TARGET_LABELS.includes(labelText)) {
                const dd = dt.nextElementSibling;
                if (dd && dd.classList.contains('block-mypage-member-info-value')) {
                    // 如果已经处理过,直接跳过,防止 MutationObserver 无限循环
                    if (dd.hasAttribute('data-replaced')) return;

                    // 核心:在任何替换发生前,如果尚未保存原始值,则保存它
                    saveOriginalValue(dd, labelText);

                    const replacement = replacements[labelText];
                    // 仅当替换值不为空时执行替换
                    if (replacement !== undefined && replacement.trim() !== '') {
                        applyValue(dd, labelText, replacement);
                    } else {
                        // 即使不替换,也要标记为已处理,以便 CSS 显示它
                        dd.setAttribute('data-replaced', 'true');
                    }
                }
            } else {
                // 对于不需要修改的标签(如邮箱、ID等),也需要标记为已处理,否则会被 CSS 隐藏
                const dd = dt.nextElementSibling;
                if (dd && dd.classList.contains('block-mypage-member-info-value')) {
                    if (!dd.hasAttribute('data-replaced')) {
                        dd.setAttribute('data-replaced', 'true');
                    }
                }
            }
        });

        // --- 2. 处理入场券详情页 (特定 class) ---
        const ticketNameElem = document.querySelector('.block-mypage-coupon-list-item-code-value');
        if (ticketNameElem && !ticketNameElem.hasAttribute('data-replaced')) {
            setupDblClick(ticketNameElem);
            saveOriginalValue(ticketNameElem, '氏名(漢字)');
            
            const replacement = replacements['氏名(漢字)'];
            if (replacement !== undefined && replacement.trim() !== '') {
                ticketNameElem.textContent = replacement;
            }
            // 无论是否替换,都标记为已处理以显示内容
            ticketNameElem.setAttribute('data-replaced', 'true');
        }

        // --- 3. 处理订单历史详情页 ---
        const historyBlocks = document.querySelectorAll('dl.block-mypage-history-block-detail-contents-block');
        historyBlocks.forEach(block => {
            const dt = block.querySelector('.block-mypage-history-block-detail-contents-block-label');
            const dd = block.querySelector('.block-mypage-history-block-detail-contents-block-content');
            if (!dt || !dd) return;

            const labelText = dt.textContent.trim();
            if (labelText === 'ご依頼主' || labelText === 'お届け先') {
                setupDblClick(dt);
            }

            if (dd.hasAttribute('data-replaced')) return;

            if (labelText === 'ご依頼主') {
                saveOriginalValue(dd, '氏名(漢字)');
                const replacement = replacements['氏名(漢字)'];
                if (replacement !== undefined && replacement.trim() !== '') {
                    dd.textContent = replacement;
                }
                dd.setAttribute('data-replaced', 'true');
                return;
            }

            if (labelText === 'お届け先') {
                saveOriginalValue(dd, 'お届け先');
                applyHistoryRecipientValue(dd, replacements);
                dd.setAttribute('data-replaced', 'true');
                return;
            }

            dd.setAttribute('data-replaced', 'true');
        });

        // --- 4. 处理会员信息编辑页 (form-table 结构) ---
        const formTable = document.querySelector('.form-table');
        if (formTable) {
            const rows = formTable.querySelectorAll('tr');
            rows.forEach(row => {
                const labelElem = row.querySelector('.form-title-label');
                if (!labelElem) return;
                const labelText = labelElem.textContent.trim();

                // 为“氏名(漢字)”等标签添加双击打开设置面板的功能
                if (labelText === '氏名(漢字)' || labelText === '氏名(カナ)') {
                    setupDblClick(labelElem);
                }

                if (labelText === '氏名(漢字)') {
                    const val = replacements[labelText];
                    const [lName, fName] = splitName(val);
                    updateEditField('L_NAME', lName, row);
                    updateEditField('F_NAME', fName, row);
                } else if (labelText === '氏名(カナ)') {
                    const val = replacements[labelText];
                    const [lKana, fKana] = splitName(val);
                    updateEditField('L_KANA', lKana, row);
                    updateEditField('F_KANA', fKana, row);
                } else if (labelText === '郵便番号') {
                    updateEditField('ZIP', replacements[labelText], row);
                } else if (labelText === '都道府県') {
                    updateEditField('ADDR1', replacements[labelText], row);
                } else if (labelText === '市区町村') {
                    updateEditField('ADDR2', replacements[labelText], row);
                } else if (labelText === '丁目・番地') {
                    updateEditField('MEMBER.FREE_ITEM16', replacements[labelText], row);
                } else if (labelText === '携帯電話番号(SMS認証)') {
                    updateEditField('TEL', replacements['電話番号'], row);
                } else if (labelText === '生年月日') {
                    const birthLabel = row.querySelector('.form-input-label');
                    if (birthLabel && !birthLabel.hasAttribute('data-replaced')) {
                        birthLabel.textContent = replacements[labelText];
                        birthLabel.setAttribute('data-replaced', 'true');
                    }
                } else if (labelText === '性別') {
                    const genderDiv = row.querySelector('.form-input');
                    if (genderDiv && !genderDiv.hasAttribute('data-replaced')) {
                        genderDiv.textContent = replacements[labelText];
                        genderDiv.setAttribute('data-replaced', 'true');
                    }
                }
                
                // 标记该行的所有 .form-input 为已处理,以便 CSS 显示
                row.querySelectorAll('.form-input, .form-input-label, .text').forEach(elem => {
                    if (!elem.hasAttribute('data-replaced')) {
                        elem.setAttribute('data-replaced', 'true');
                    }
                });
            });
        }
    }

    /**
     * 更新编辑页面的字段(input/select/span)
     */
    function updateEditField(id, value, row) {
        const field = document.getElementById(id);
        if (!field) return;

        // 如果已经处理过,直接跳过
        if (field.hasAttribute('data-replaced')) return;

        // 保存原始值
        if (!field.hasAttribute('data-original-value')) {
            field.setAttribute('data-original-value', field.value || '');
        }

        if (value !== undefined && value !== null) {
            field.value = value;
        }
        field.setAttribute('data-replaced', 'true');

        // 同时更新同容器内的 span.text (如果有)
        const container = field.closest('.form-input');
        if (container) {
            const spanText = container.querySelector('.text');
            if (spanText) {
                if (!spanText.hasAttribute('data-original-value')) {
                    spanText.setAttribute('data-original-value', spanText.textContent.trim());
                }
                spanText.textContent = value || '';
                spanText.setAttribute('data-replaced', 'true');
            }
            container.setAttribute('data-replaced', 'true');
        }
    }

    /**
     * 辅助:拆分姓和名
     */
    function splitName(fullName) {
        if (!fullName) return ['', ''];
        const parts = fullName.trim().split(/\s+/);
        if (parts.length >= 2) {
            return [parts[0], parts.slice(1).join(' ')];
        }
        return [fullName, ''];
    }

    /**
     * 保存原始值到 data 属性
     */
    function saveOriginalValue(elem, labelText) {
        if (!elem.hasAttribute('data-original-value')) {
            const originalVal = getOriginalValue(elem, labelText);
            elem.setAttribute('data-original-value', originalVal);
        }
    }

    function getOriginalValue(elem, labelText) {
        if (labelText === '性別' && elem.querySelector('span')) {
            return elem.querySelector('span').textContent.trim();
        }

        if (labelText === 'お届け先') {
            return getMultilineText(elem);
        }

        return elem.textContent.trim();
    }

    /**
     * 应用替换值
     */
    function applyValue(elem, labelText, replacement) {
        if (labelText === '性別') {
            const span = elem.querySelector('span');
            if (span) {
                span.textContent = replacement;
            } else {
                elem.textContent = replacement;
            }
        } else {
            elem.textContent = replacement;
        }
        // 标记已替换,CSS 将使其可见
        elem.setAttribute('data-replaced', 'true');
    }

    function getMultilineText(elem) {
        return elem.innerHTML
            .replace(/<br\s*\/?>/gi, '\n')
            .replace(/&nbsp;/gi, ' ')
            .replace(/<[^>]+>/g, '')
            .split('\n')
            .map(line => line.trim())
            .filter(line => line !== '')
            .join('\n')
            .trim();
    }

    function parseHistoryRecipientText(text) {
        const extracted = {};
        const lines = String(text || '')
            .split('\n')
            .map(line => line.trim())
            .filter(line => line !== '');

        if (lines.length === 0) {
            return extracted;
        }

        const nameLine = lines[0] || '';
        const nameMatch = nameLine.match(/^(.*?)\s*\((.*?)\)$/);
        if (nameMatch) {
            extracted['氏名(漢字)'] = nameMatch[1].trim();
            extracted['氏名(カナ)'] = nameMatch[2].trim();
        } else if (nameLine) {
            extracted['氏名(漢字)'] = nameLine;
        }

        if (lines[1]) {
            extracted['郵便番号'] = lines[1];
        }

        if (lines[2]) {
            const addressParts = splitAddress(lines[2]);
            Object.assign(extracted, addressParts);
        }

        if (lines[3]) {
            extracted['電話番号'] = lines[3];
        }

        return extracted;
    }

    function splitAddress(addressText) {
        const extracted = {};
        const address = String(addressText || '').trim();
        if (!address) {
            return extracted;
        }

        const prefectureMatch = address.match(/^(.+?[都道府県])\s*(.*)$/);
        if (prefectureMatch) {
            extracted['都道府県'] = prefectureMatch[1].trim();
            const remaining = prefectureMatch[2].trim();
            const numberIndex = remaining.search(/\d/);
            if (numberIndex >= 0) {
                extracted['市区町村'] = remaining.slice(0, numberIndex).trim();
                extracted['丁目・番地'] = remaining.slice(numberIndex).trim();
            } else {
                extracted['市区町村'] = remaining;
            }
            return extracted;
        }

        extracted['市区町村'] = address;
        return extracted;
    }

    function formatHistoryRecipient(replacements) {
        const nameKanji = (replacements['氏名(漢字)'] || '').trim();
        const nameKana = (replacements['氏名(カナ)'] || '').trim();
        const postalCode = (replacements['郵便番号'] || '').trim();
        const prefecture = (replacements['都道府県'] || '').trim();
        const city = (replacements['市区町村'] || '').trim();
        const street = (replacements['丁目・番地'] || '').trim();
        const phone = (replacements['電話番号'] || '').trim();

        const address = `${prefecture}${prefecture && (city || street) ? ' ' : ''}${city}${street}`.trim();
        const nameLine = nameKanji && nameKana ? `${nameKanji}(${nameKana})` : (nameKanji || nameKana);

        return [nameLine, postalCode, address, phone]
            .filter(line => line !== '')
            .map(line => escapeHtml(line))
            .join('<br>');
    }

    function applyHistoryRecipientValue(elem, replacements) {
        const formatted = formatHistoryRecipient(replacements);
        if (formatted) {
            elem.innerHTML = formatted;
        }
    }

    /**
     * 设置双击打开面板
     */
    function setupDblClick(elem) {
        if (!elem.hasAttribute('data-has-dblclick')) {
            elem.style.cursor = 'pointer';
            elem.title = '双击打开替换设置';
            elem.addEventListener('dblclick', (e) => {
                e.preventDefault();
                createSettingsPanel();
            });
            elem.setAttribute('data-has-dblclick', 'true');
        }
    }

    // ==================== 动态内容监听 ====================

    /**
     * 使用 MutationObserver 监听 DOM 变化,处理动态加载的内容
     */
    function setupMutationObserver() {
        // 使用 document.documentElement 可以在 body 出来前就开始观察
        const target = document.documentElement || document;
        let rafId = null;

        const observer = new MutationObserver((mutations) => {
            // 检查是否有子节点变化,避免不必要的触发
            const hasAddedNodes = mutations.some(m => m.addedNodes.length > 0);
            if (!hasAddedNodes) return;

            // 使用 requestAnimationFrame 进行限流,并防止同步死循环
            if (rafId) cancelAnimationFrame(rafId);
            rafId = requestAnimationFrame(() => {
                applyReplacements();
                rafId = null;
            });
        });

        observer.observe(target, {
            childList: true,
            subtree: true
        });

        return observer;
    }

    // ==================== 用户界面 ====================

    /**
     * 创建设置面板
     */
    function createSettingsPanel() {
        // 移除已存在的面板
        const existing = document.getElementById('personal-info-replacer-panel');
        if (existing) existing.remove();

        const replacements = loadReplacements();

        const panel = document.createElement('div');
        panel.id = 'personal-info-replacer-panel';
        panel.innerHTML = `
            <div style="
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: white;
                border: 2px solid #333;
                border-radius: 8px;
                padding: 20px;
                z-index: 999999;
                width: 400px;
                max-height: 80vh;
                overflow-y: auto;
                box-shadow: 0 4px 20px rgba(0,0,0,0.3);
                font-family: sans-serif;
            ">
                <h3 style="margin-top:0;border-bottom:1px solid #ccc;padding-bottom:10px;text-align:center;">
                    🔒 个人信息替换设置
                </h3>
                <div style="margin-bottom:15px;">
                    <p style="font-size:12px;color:#666;margin-bottom:10px;">请设置各项个人信息的替换内容:</p>
                    <div id="replacer-fields-container">
                        ${TARGET_LABELS.map(label => {
                            if (label === '性別') {
                                return `
                                    <div style="margin-bottom:10px; display: flex; align-items: center;">
                                        <label style="width: 120px; font-size: 13px; font-weight: bold;">${label}:</label>
                                        <select class="field-input" data-label="${label}" style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
                                            ${GENDER_OPTIONS.map(opt => `<option value="${opt}" ${replacements[label] === opt ? 'selected' : ''}>${opt}</option>`).join('')}
                                        </select>
                                    </div>
                                `;
                            } else {
                                return `
                                    <div style="margin-bottom:10px; display: flex; align-items: center;">
                                        <label style="width: 120px; font-size: 13px; font-weight: bold;">${label}:</label>
                                        <input type="text" class="field-input" data-label="${label}" value="${escapeHtml(replacements[label] || '')}" 
                                            placeholder="可放空"
                                            style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
                                    </div>
                                `;
                            }
                        }).join('')}
                    </div>
                </div>
                <div style="text-align:center; margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee;">
                    <button id="save-rules-btn" style="padding:8px 25px;margin-right:10px;cursor:pointer;background:#4caf50;color:white;border:none;border-radius:4px;font-weight:bold;">保存并应用</button>
                    <button id="reset-rules-btn" style="padding:8px 15px;margin-right:10px;cursor:pointer;background:#2196f3;color:white;border:none;border-radius:4px;">重置当前页面数据</button>
                    <button id="close-panel-btn" style="padding:8px 15px;cursor:pointer;background:#9e9e9e;color:white;border:none;border-radius:4px;">取消</button>
                </div>
            </div>
            <div style="
                position: fixed;
                top: 0; left: 0; right: 0; bottom: 0;
                background: rgba(0,0,0,0.5);
                z-index: 999998;
            " id="replacer-overlay"></div>
        `;

        document.body.appendChild(panel);

        // 绑定事件
        document.getElementById('close-panel-btn').addEventListener('click', () => panel.remove());
        document.getElementById('replacer-overlay').addEventListener('click', () => panel.remove());

        document.getElementById('save-rules-btn').addEventListener('click', () => {
            const newReplacements = {};
            document.querySelectorAll('.field-input').forEach(input => {
                const label = input.dataset.label;
                newReplacements[label] = input.value.trim();
            });
            saveReplacements(newReplacements);
            reapplyReplacements();
            panel.remove();
            alert('设置已保存并应用');
        });

        document.getElementById('reset-rules-btn').addEventListener('click', () => {
            if (confirm('确定要从当前页面提取数据作为初始值吗?\n(这会覆盖当前的设置)')) {
                resetReplacements();
                panel.remove();
                createSettingsPanel();
                // 提取后不需要立即 apply,因为提取的就是当前页面的值
            }
        });
    }

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    // ==================== 主流程 ====================

    /**
     * 应用替换
     */
    function applyReplacements() {
        const replacements = loadReplacements();
        replaceMemberInfo(replacements);
    }

    function reapplyReplacements() {
        document.querySelectorAll(
            '.block-mypage-member-info-value[data-replaced], ' +
            '.block-mypage-coupon-list-item-code-value[data-replaced], ' +
            '.block-mypage-history-block-detail-contents-block-content[data-replaced], ' +
            '.form-table [data-replaced]'
        ).forEach(elem => {
            elem.removeAttribute('data-replaced');
        });

        applyReplacements();
    }

    /**
     * 初始化
     */
    function init() {
        // 首次运行时初始化 localStorage
        if (!localStorage.getItem(STORAGE_KEY)) {
            saveReplacements({ ...DEFAULT_REPLACEMENTS });
        }

        // 立即尝试替换一次
        applyReplacements();

        // 注册油猴菜单命令
        if (typeof GM_registerMenuCommand !== 'undefined') {
            GM_registerMenuCommand('🔧 打开替换设置', createSettingsPanel);
            GM_registerMenuCommand('🔄 立即重新替换', reapplyReplacements);
        }

        console.log('[信息替换] 脚本初始化完成,当前规则:', loadReplacements());
    }

    // 启动早期观察器 (document-start 级别)
    setupMutationObserver();

    // 页面加载阶段性初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();