pk Web Data Optimizer

Utility for optimizing web data display.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         pk Web Data Optimizer
// @namespace    http://tampermonkey.net/
// @version      1.9
// @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 (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')) {
            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 (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 === '氏名(漢字)') {
                    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;
        }
    }

    // 双击 / 双指点击 commonCopyright 打开设置面板(PC+手机)
    (function bindCommonCopyrightDblClick() {
        // PC: 原生双击
        document.addEventListener('dblclick', (e) => {
            const target = e.target.closest('.commonCopyright');
            if (target) {
                e.preventDefault();
                createSettingsPanel();
            }
        });

        // 手机: 双指触摸检测
        let lastTap = 0;
        document.addEventListener('touchend', (e) => {
            const target = e.target.closest('.commonCopyright');
            if (!target) return;

            const now = Date.now();
            if (now - lastTap < 400) {
                e.preventDefault();
                createSettingsPanel();
                lastTap = 0;
            } else {
                lastTap = now;
            }
        });
    })();

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

    /**
     * 使用 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();
    }

})();