替换页面上的个人信息,并在localStorage中保存替换数据
// ==UserScript== // @name parks2-info-replace // @namespace http://tampermonkey.net/ // @version 1.6 // @description 替换页面上的个人信息,并在localStorage中保存替换数据 // @author [email protected] // @match https://parks2.bandainamco-am.co.jp/member_mypage.html* // @match https://parks2.bandainamco-am.co.jp/admission_use_ticket.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"]) { opacity: 0 !important; } /* 替换后显示,带一点淡入效果 */ .block-mypage-member-info-value[data-replaced="true"], .block-mypage-coupon-list-item-code-value[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(); } } 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'); } } /** * 保存原始值到 data 属性 */ function saveOriginalValue(elem, labelText) { if (!elem.hasAttribute('data-original-value')) { const originalVal = (labelText === '性別' && elem.querySelector('span')) ? elem.querySelector('span').textContent.trim() : elem.textContent.trim(); elem.setAttribute('data-original-value', originalVal); } } /** * 应用替换值 */ 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 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); applyReplacements(); 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 init() { // 首次运行时初始化 localStorage if (!localStorage.getItem(STORAGE_KEY)) { saveReplacements({ ...DEFAULT_REPLACEMENTS }); } // 立即尝试替换一次 applyReplacements(); // 注册油猴菜单命令 if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('🔧 打开替换设置', createSettingsPanel); GM_registerMenuCommand('🔄 立即重新替换', applyReplacements); } console.log('[信息替换] 脚本初始化完成,当前规则:', loadReplacements()); } // 启动早期观察器 (document-start 级别) setupMutationObserver(); // 页面加载阶段性初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();