// ==UserScript== // @name NAMCO Parks 改个人信息 *(通用版本) // @namespace https://parks2.bandainamco-am.co.jp/ // @version 1.6.0 // @description 改会员资料姓名/生日/性别;可隐藏按钮与券面强制显示;支持 Excel 复制快速填充 // @grant unsafeWindow // @author park-tools // @match https://parks2.bandainamco-am.co.jp/* // @icon https://parks2.bandainamco-am.co.jp/client_info/BNAM_LBC_EC/view/userweb/favicon.ico // @run-at document-end // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function () { 'use strict'; const ORIGIN = 'https://parks2.bandainamco-am.co.jp'; const LS_KEY = 'namco_rename_draft_v1'; const LS_OVERLAY = 'namco_ticket_overlay_v1'; const LS_HIDE_UI = 'namco_hide_plugin_ui_v1'; const TICKET_PATH_RE = /\/admission_(use_)?ticket\.html/i; const PAGE = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; function isLoggedInFromDom() { if (document.querySelector('a[href*="logoff"], a[href*="request=logoff"]')) return true; const html = document.documentElement.innerHTML; if (html.includes('ログアウト')) return true; return !!parseMemberData(html).member_id; } function htmlLooksLoggedIn(html) { if (!html) return false; if (html.includes('ログアウト')) return true; if (parseMemberData(html).member_id) return true; if (parseInput(html, 'PC_MAIL') && (parseInput(html, 'TEL') || parseInput(html, 'L_NAME'))) return true; return false; } /** iOS Tampermonkey 沙箱 fetch 不带 Cookie;结果放页面 window,避免把整页 HTML 塞进 DOM 属性被截断 */ function pageFetch(url, options) { return new Promise((resolve, reject) => { const id = '__npFetch_' + Date.now() + '_' + Math.random().toString(36).slice(2); const opt = { method: (options && options.method) || 'GET', credentials: 'same-origin', redirect: 'follow', headers: (options && options.headers) || {}, }; if (options && options.body) opt.body = options.body; const win = PAGE; const script = document.createElement('script'); script.textContent = '(function(){var id=' + JSON.stringify(id) + ';window[id]={p:1};fetch(' + JSON.stringify(url) + ',' + JSON.stringify(opt) + ').then(function(r){return r.text().then(function(t){window[id]={s:r.status,u:r.url,t:t};});}).catch(function(e){window[id]={e:String(e&&e.message||e)};});})();'; document.documentElement.appendChild(script); script.remove(); const start = Date.now(); const timer = setInterval(() => { const box = (win && win[id]) || window[id]; if (box && box.e) { clearInterval(timer); try { delete win[id]; } catch (e) { /* ignore */ } reject(new Error(box.e)); return; } if (box && typeof box.t === 'string') { clearInterval(timer); const out = { status: Number(box.s || 0), url: box.u || url, text: box.t }; try { delete win[id]; } catch (e) { /* ignore */ } resolve(out); return; } if (Date.now() - start > 90000) { clearInterval(timer); try { delete win[id]; } catch (e) { /* ignore */ } reject(new Error('请求超时')); } }, 40); }); } async function httpGet(path, referer) { const url = path.startsWith('http') ? path : ORIGIN + path; const headers = { Referer: referer || ORIGIN + '/member_mypage.html' }; try { return await pageFetch(url, { method: 'GET', headers }); } catch (e1) { try { const r = await PAGE.fetch(url, { method: 'GET', credentials: 'include', headers }); return { status: r.status, text: await r.text(), url: r.url }; } catch (e2) { throw e1; } } } async function httpPost(path, body, referer) { const url = path.startsWith('http') ? path : ORIGIN + path; const headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Origin: ORIGIN, Referer: referer || ORIGIN + '/member_regist.html?request=edit', }; const bodyStr = new URLSearchParams(body).toString(); try { return await pageFetch(url, { method: 'POST', headers, body: bodyStr }); } catch (e1) { try { const r = await PAGE.fetch(url, { method: 'POST', credentials: 'include', headers, body: bodyStr }); return { status: r.status, text: await r.text(), url: r.url }; } catch (e2) { throw e1; } } } const store = { get(k, def) { try { if (typeof GM_getValue === 'function') return GM_getValue(k, def); } catch (e) { /* ignore */ } try { const raw = localStorage.getItem(k); return raw == null ? def : JSON.parse(raw); } catch (e2) { return def; } }, set(k, v) { try { if (typeof GM_setValue === 'function') GM_setValue(k, v); } catch (e) { /* ignore */ } try { localStorage.setItem(k, JSON.stringify(v)); } catch (e2) { /* ignore */ } }, }; function $(sel, root) { return (root || document).querySelector(sel); } function escapeRe(name) { return String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function parseInput(html, name) { const re = new RegExp( ']*\\bname=["\']' + escapeRe(name) + '["\'][^>]*>', 'i' ); const m = html.match(re); if (!m) return ''; const v = m[0].match(/\bvalue=["']([^"']*)["']/i); return v ? v[1].trim() : ''; } function parseSelected(html, name) { const sm = html.match( new RegExp(']*name=["\']' + escapeRe(name) + '["\'][^>]*>([\\s\\S]*?)', 'i') ); if (!sm) return ''; const opt = sm[1].match(/]*\\bselected\\b[^>]*>/i) || sm[1].match(/]*selected=["']selected["'][^>]*>/i); if (!opt) return ''; const v = opt[0].match(/\\bvalue=["']([^"']*)["']/i); return v ? v[1].trim() : ''; } function parseCheckedRadio(html, name) { const re = new RegExp( ']*\\bname=["\']' + escapeRe(name) + '["\'][^>]*>', 'gi' ); let m; while ((m = re.exec(html))) { const tag = m[0]; if (!/\bchecked\b/i.test(tag)) continue; const v = tag.match(/\bvalue=["']([^"']*)["']/i); return v ? v[1] : ''; } return ''; } function parseFormChunk(html, formName) { const head = html.match( new RegExp(']*name=["\']' + escapeRe(formName) + '["\'][^>]*>', 'i') ); const body = html.match( new RegExp(']*name=["\']' + escapeRe(formName) + '["\'][^>]*>([\\s\\S]*?)', 'i') ); let action = ''; if (head) { const am = head[0].match(/\baction=["']([^"']+)/i); if (am) action = am[1]; } return { action, chunk: body ? body[1] : '' }; } function parseHiddenFields(chunk) { const fields = {}; const re = /]*>/gi; let m; while ((m = re.exec(chunk))) { const tag = m[0]; if (!/type=["']hidden["']/i.test(tag) && !/type=["']checkbox["']/i.test(tag) && !/type=["']radio["']/i.test(tag)) { const nm = tag.match(/\bname=["']([^"']+)["']/i); const vm = tag.match(/\bvalue=["']([^"']*)["']/i); if (nm && !/^jp\.co\.interfactory\.framework\./i.test(nm[1])) { fields[nm[1]] = vm ? vm[1] : ''; } continue; } const nm = tag.match(/\bname=["']([^"']+)["']/i); if (!nm) continue; const name = nm[1]; if (/^jp\.co\.interfactory\.framework\./i.test(name)) continue; if (/type=["']checkbox["']/i.test(tag)) { if (/\bchecked\b/i.test(tag)) { const vm = tag.match(/\bvalue=["']([^"']*)["']/i); fields[name] = vm ? vm[1] : '1'; } continue; } if (/type=["']radio["']/i.test(tag)) { if (/\bchecked\b/i.test(tag)) { const vm = tag.match(/\bvalue=["']([^"']*)["']/i); fields[name] = vm ? vm[1] : ''; } continue; } const vm = tag.match(/\bvalue=["']([^"']*)["']/i); fields[name] = vm ? vm[1] : ''; } const selRe = /]*name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/select>/gi; let sm; while ((sm = selRe.exec(chunk))) { const name = sm[1]; const opt = sm[2].match(/]*\bselected\b[^>]*>/i); if (!opt) continue; const v = opt[0].match(/\bvalue=["']([^"']*)["']/i); fields[name] = v ? v[1] : ''; } return fields; } function extractParksError(html) { const text = html || ''; const pats = [ /form-error-message[\s\S]*?
  • ([^<]+)/i, /]*>([^<]{4,200})<\/li>/i, /class="error[^"]*"[^>]*>([^<]+)/i, /errorMessage[^>]*>([^<]+)/i, ]; for (let i = 0; i < pats.length; i++) { const m = text.match(pats[i]); if (m && m[1] && m[1].trim()) return m[1].replace(/\s+/g, ' ').trim(); } if (text.includes('セッションがタイムアウト') || text.includes('セキュリティのため')) { return '会话超时,请刷新页面重新登录后再提交'; } return ''; } function looksExecuteSuccess(resp) { const t = (resp && resp.text) || ''; const u = (resp && resp.url) || ''; if (t.includes('会員情報を更新しました')) return true; if (t.includes('form-message') && t.includes('更新しました')) return true; if (u.includes('member_regist_confirm') && t.includes('更新')) return true; return false; } function parseToken(html) { const m = (html || '').match(/name="token"\s+value="([0-9a-f]+)"/i); return m ? m[1] : ''; } function parseMemberData(html) { const m = (html || '').match(/var\s+member_data\s*=\s*(\{[\s\S]*?\})\s*;/); if (!m) return {}; try { return JSON.parse(m[1]); } catch (e) { return {}; } } function parseProfile(html) { const md = parseMemberData(html); let tel = parseInput(html, 'TEL'); if (!tel) { const tm = html.match(/(?= 2) return { l: parts[0], f: parts.slice(1).join(' ') }; return { l: s.charAt(0), f: s.slice(1) || s }; } return { l: s.charAt(0), f: s.slice(1) }; } function getOverlayConfig() { return store.get(LS_OVERLAY, { enabled: false, displayName: '' }); } function setOverlayConfig(cfg) { store.set(LS_OVERLAY, cfg); } function getHideUi() { const saved = store.get(LS_HIDE_UI, null); if (saved != null) return saved; return { hidden: false }; } function setHideUi(cfg) { store.set(LS_HIDE_UI, cfg); } function shouldHidePluginUi() { const cfg = getHideUi(); return !!(cfg && cfg.hidden); } function buildDisplayName(l, f, full) { if (full && full.trim()) return full.trim().replace(/\s+/g, ' '); return `${l || ''} ${f || ''}`.trim(); } function isTicketPage() { return TICKET_PATH_RE.test(location.pathname + location.search); } function getTicketNameDl() { const dls = document.querySelectorAll('dl.block-mypage-ticket-detail-code'); for (let i = 0; i < dls.length; i++) { const dl = dls[i]; if (dl.classList.contains('block-mypage-ticket-detail-code-margin-small')) continue; if (dl.querySelector('dd.block-mypage-ticket-detail-code-value')) return dl; } return null; } function injectOverlayStyles() { const css = 'dd[data-np-overlay="1"],dd.np-injected-name{' + 'display:block!important;visibility:visible!important;opacity:1!important;' + '-webkit-text-fill-color:currentColor!important}'; let st = document.getElementById('np-overlay-style'); if (!st) { st = document.createElement('style'); st.id = 'np-overlay-style'; document.head.appendChild(st); } st.textContent = css; } function ensureNameSlot() { const dl = getTicketNameDl(); if (!dl) return null; let nameDd = null; dl.querySelectorAll('dd.block-mypage-coupon-list-item-code-value').forEach((dd) => { if (nameDd) return; const t = (dd.textContent || '').trim(); if (!/^EC-\d/i.test(t) && !/^\d+$/.test(t)) nameDd = dd; }); if (!nameDd) { nameDd = document.createElement('dd'); nameDd.className = 'block-mypage-coupon-list-item-code-value np-injected-name'; const ec = dl.querySelector('dd.block-mypage-ticket-detail-code-value'); if (ec) dl.insertBefore(nameDd, ec); else dl.appendChild(nameDd); } return nameDd; } function findTicketNameNodes(scope, createIfMissing) { const root = scope || document; const nodes = []; const seen = new Set(); if (createIfMissing) { const slot = ensureNameSlot(); if (slot && !seen.has(slot)) { seen.add(slot); nodes.push(slot); } } root.querySelectorAll('dl.block-mypage-ticket-detail-code dd.block-mypage-coupon-list-item-code-value').forEach((dd) => { if (seen.has(dd)) return; const t = (dd.textContent || '').trim(); if (/^EC-\d/i.test(t)) return; if (/^\d+$/.test(t)) return; seen.add(dd); nodes.push(dd); }); return nodes; } function restoreTicketNames() { document.querySelectorAll('dd.np-injected-name').forEach((el) => el.remove()); findTicketNameNodes(document, false).forEach((el) => { if (el.dataset.npOrig != null) { el.textContent = el.dataset.npOrig; delete el.dataset.npPatched; delete el.dataset.npOverlay; } }); } function applyTicketOverlay(force) { const cfg = getOverlayConfig(); if (!cfg.enabled || !cfg.displayName) { restoreTicketNames(); return 0; } if (!isTicketPage() && !force) return 0; injectOverlayStyles(); let n = 0; const nodes = findTicketNameNodes(document, true); nodes.forEach((el) => { const cur = (el.textContent || '').trim(); if (el.dataset.npOrig == null && cur && cur !== cfg.displayName) { el.dataset.npOrig = cur; } if (cur !== cfg.displayName || el.dataset.npPatched !== '1') { el.textContent = cfg.displayName; el.dataset.npOverlay = '1'; el.dataset.npPatched = '1'; n += 1; } }); return n; } function startOverlayWatcher() { if (window.__npOverlayWatcher) return; window.__npOverlayWatcher = true; const run = () => { if (!getOverlayConfig().enabled) return; applyTicketOverlay(); }; run(); document.addEventListener('DOMContentLoaded', run); window.addEventListener('load', run); window.addEventListener('pageshow', run); const mo = new MutationObserver(() => { if (!getOverlayConfig().enabled) return; clearTimeout(window.__npOverlayTimer); window.__npOverlayTimer = setTimeout(run, 80); }); mo.observe(document.documentElement, { childList: true, subtree: true, characterData: true }); let lastUrl = location.href; setInterval(() => { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(run, 100); } }, 500); } startOverlayWatcher(); async function checkLoggedIn() { if (isLoggedInFromDom()) return true; try { const r = await httpGet('/member_mypage.html'); return htmlLooksLoggedIn(r.text); } catch (e) { return isLoggedInFromDom(); } } async function loadProfile() { await httpGet('/member_mypage.html'); const r = await httpGet('/member_regist.html?request=edit'); if (!htmlLooksLoggedIn(r.text)) { if (isLoggedInFromDom()) { throw new Error('已登录但读取资料失败,请刷新页面后重试'); } throw new Error('未登录:请用 Safari 打开 parks2 并完成登录(不要用无痕模式)'); } const p = parseProfile(r.text); if (!p.tel) throw new Error('未读取到手机号,无法安全提交'); return p; } function normalizeBirthday(raw) { const s = String(raw || '').trim(); if (!s) return ''; const m1 = s.match(/^(\d{4})[-/\.](\d{1,2})[-/\.](\d{1,2})/); if (m1) { return `${m1[1]}-${String(parseInt(m1[2], 10)).padStart(2, '0')}-${String(parseInt(m1[3], 10)).padStart(2, '0')}`; } const digits = s.replace(/\D/g, ''); if (digits.length === 8) { return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`; } return ''; } function bdayParts(bday) { const norm = normalizeBirthday(bday) || '1990-01-01'; const m = norm.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/); if (!m) return { y: '1990', mo: '1', d: '1' }; return { y: m[1], mo: String(parseInt(m[2], 10)), d: String(parseInt(m[3], 10)), }; } async function updateMemberName(profile, changes, password) { const ln = changes.last_name || profile.last_name; const fn = changes.first_name || profile.first_name; const lk = changes.last_name_kana != null ? changes.last_name_kana : profile.last_name_kana; const fk = changes.first_name_kana != null ? changes.first_name_kana : profile.first_name_kana; const nick = changes.nickname != null ? changes.nickname : (profile.nickname || ln); const bday = normalizeBirthday(changes.birthday || profile.birthday) || profile.birthday || '1990-01-01'; const { y, mo, d } = bdayParts(bday); const editRef = ORIGIN + '/member_regist.html?request=edit'; const zip7 = String(profile.zip || '').replace(/-/g, ''); const addr1 = profile.addr1 || '東京都'; const addr2 = profile.addr2 || ''; const addrStreet = profile.addr_street || ''; const addr3 = profile.addr3 || ''; const sex = changes.gender || profile.gender || 'M'; if (!zip7 || !addr2 || !addrStreet) { throw new Error('当前资料缺邮编/市区町村/番地(官网已改为必填)。请先在「会員情報変更」填完整地址,再回来改名字。'); } const confirm = Object.assign({}, profile.formFields || {}, { request: 'confirm', PC_MAIL_OLD: profile.email, FOREIGN_LOGIN_PROVIDER_KIND: '', MOBILE_MAIL_OLD: '', mode: '1', CART_MEMBER_REGIST: '', MAIL_FLG_OLD: '1', 'SOCIAL_PLUS:SOCIAL_PLUS_ID': '', 'SOCIAL_PLUS:PROVIDER': '', NICKNAME: nick, 'jp.co.interfactory.framework.trim.NICKNAME': '', PC_MAIL: profile.email, 'jp.co.interfactory.framework.trim.PC_MAIL': '', PASSWORD: password, PASSWORD2: password, BIRTH_YEAR: y, 'jp.co.interfactory.framework.trim.BIRTH_YEAR': '', BIRTH_MONTH: mo, 'jp.co.interfactory.framework.trim.BIRTH_MONTH': '', BIRTH_DAY: d, 'jp.co.interfactory.framework.trim.BIRTH_DAY': '', SEX: sex, ZIP: zip7, 'jp.co.interfactory.framework.trim.ZIP': '', ADDR1: addr1, ADDR2: addr2, 'jp.co.interfactory.framework.trim.ADDR2': '', 'MEMBER.FREE_ITEM16': addrStreet, 'jp.co.interfactory.framework.trim.MEMBER.FREE_ITEM16': '', ADDR3: addr3, 'jp.co.interfactory.framework.trim.ADDR3': '', TEL: profile.tel, 'jp.co.interfactory.framework.trim.TEL': '', L_NAME: ln, F_NAME: fn, L_KANA: lk, 'jp.co.interfactory.framework.trim.L_KANA': '', F_KANA: fk, 'jp.co.interfactory.framework.trim.F_KANA': '', PC_MAIL_TYPE: '1', MOBILE_MAIL_TYPE: '1', }); if (!addr3) confirm['MEMBER.FREE_ITEM19'] = '1'; else delete confirm['MEMBER.FREE_ITEM19']; const r1 = await httpPost('/member_regist.html', confirm, editRef); if (r1.text.includes('sms_authentication') || r1.url.includes('sms_authentication')) { throw new Error('触发了 SMS 验证(请勿改手机号)'); } const confirmParsed = parseFormChunk(r1.text, 'confirmForm'); const hidden = parseHiddenFields(confirmParsed.chunk); const token = hidden.token || parseToken(r1.text); if (!token) { throw new Error(extractParksError(r1.text) || 'confirm 失败,请检查密码是否正确'); } const execute = Object.assign({}, hidden, { request: 'execute', token, MAIL_FLG: hidden.MAIL_FLG || '1', BIRTH_YEAR: y, BIRTH_MONTH: mo, BIRTH_DAY: d, BIRTH: y + '/' + mo + '/' + d, SEX: sex, ZIP: zip7 || hidden.ZIP || '', ADDR1: addr1 || hidden.ADDR1 || '', ADDR2: addr2 || hidden.ADDR2 || '', 'MEMBER.FREE_ITEM16': addrStreet || hidden['MEMBER.FREE_ITEM16'] || '', ADDR3: addr3 || hidden.ADDR3 || '', TEL: profile.tel, L_NAME: ln, F_NAME: fn, L_KANA: lk, F_KANA: fk, NICKNAME: nick, PC_MAIL: profile.email, PASSWORD: password, PASSWORD2: password, }); if (addr3) delete execute['MEMBER.FREE_ITEM19']; else execute['MEMBER.FREE_ITEM19'] = '1'; const action = confirmParsed.action || '/member_regist_confirm.html'; const r2 = await httpPost(action, execute, ORIGIN + '/member_regist.html'); if (r2.text.includes('sms_authentication') || r2.url.includes('sms_authentication')) { throw new Error('execute 触发 SMS'); } if (!looksExecuteSuccess(r2)) { const afterEdit = await httpGet('/member_regist.html?request=edit', ORIGIN + '/member_mypage.html'); const after = parseProfile(afterEdit.text); if (after.last_name === ln && after.first_name === fn) { return { last_name: ln, first_name: fn, last_name_kana: lk, first_name_kana: fk, birthday: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`, gender: sex, }; } throw new Error(extractParksError(r2.text) || 'execute 未返回成功页'); } return { last_name: ln, first_name: fn, last_name_kana: lk, first_name_kana: fk, birthday: `${y}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}`, gender: sex, }; } async function verifyTicketNames() { const r = await httpGet('/admission_ticket.html'); const orders = [...r.text.matchAll(/admission_use_ticket\.html\?order_no=(\d+)/g)].map((m) => m[1]); const tickets = []; for (const ono of orders) { const t = await httpGet('/admission_use_ticket.html?order_no=' + ono, ORIGIN + '/admission_ticket.html'); const m = t.text.match( /block-mypage-coupon-list-item-code-value">([^<]+)<\/dd>\s*
    (EC-\d+)<\/dd>/s ); if (m) tickets.push({ order: ono, ec: m[2], name: m[1].trim() }); } const hist = await httpGet('/member_history.html'); const clients = [...hist.text.matchAll(/ご依頼主<\/dt>\s*]*>\s*([^<]+)/g)].map((m) => m[1].trim()); const prof = await loadProfile(); const member = `${prof.last_name} ${prof.first_name}`.trim(); return { member, tickets, clients, kana: `${prof.last_name_kana} ${prof.first_name_kana}`.trim() }; } /* ---------- UI ---------- */ const css = ` #npRenameRoot{all:initial;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;} #npRenameFab{position:fixed;right:14px;bottom:calc(18px + env(safe-area-inset-bottom));z-index:2147483646;width:54px;height:54px;border-radius:27px;border:none;background:linear-gradient(135deg,#e60012,#b8000f);color:#fff;font-size:14px;font-weight:700;box-shadow:0 4px 16px rgba(0,0,0,.35);cursor:pointer;} #npRenameMask{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:2147483647;display:none;} #npRenamePanel{position:fixed;left:0;right:0;bottom:0;max-height:88vh;overflow:auto;background:#fff;border-radius:16px 16px 0 0;padding:16px 16px calc(20px + env(safe-area-inset-bottom));z-index:2147483647;transform:translateY(110%);transition:transform .25s ease;box-sizing:border-box;} #npRenamePanel.open{transform:translateY(0);} #npRenamePanel *{box-sizing:border-box;font-family:inherit;} .np-title{font-size:17px;font-weight:700;margin:0 0 4px;color:#111;} .np-sub{font-size:12px;color:#666;margin:0 0 12px;line-height:1.5;} .np-warn{font-size:11px;color:#b45309;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:8px 10px;margin-bottom:12px;line-height:1.45;} .np-row{margin-bottom:10px;} .np-row label{display:block;font-size:12px;color:#444;margin-bottom:4px;} .np-row input, .np-row select, .np-row textarea{width:100%;border:1px solid #ddd;border-radius:8px;padding:0 12px;font-size:16px;background:#fff;} .np-row input, .np-row select{height:42px;} .np-row textarea{padding:8px 12px;font-size:14px;resize:vertical;} .np-row input:focus, .np-row select:focus, .np-row textarea:focus{outline:none;border-color:#e60012;} .np-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;} .np-btns{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px;} .np-btn{height:44px;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;} .np-btn-primary{background:#e60012;color:#fff;} .np-btn-secondary{background:#f3f4f6;color:#111;} .np-btn-full{grid-column:1/-1;} .np-log{margin-top:12px;font-size:12px;line-height:1.55;color:#333;background:#f9fafb;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:160px;overflow:auto;} .np-close{position:absolute;right:12px;top:12px;border:none;background:#eee;width:32px;height:32px;border-radius:16px;font-size:18px;cursor:pointer;} .np-switch-box{background:linear-gradient(135deg,#ecfdf5,#f0fdf4);border:1px solid #6ee7b7;border-radius:12px;padding:12px;margin-bottom:12px;} .np-switch-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px;} .np-switch-title{font-size:14px;font-weight:700;color:#065f46;} .np-switch-hint{font-size:11px;color:#047857;line-height:1.45;margin:0 0 8px;} .np-switch{position:relative;width:52px;height:30px;flex-shrink:0;} .np-switch input{opacity:0;width:0;height:0;} .np-switch-slider{position:absolute;inset:0;background:#cbd5e1;border-radius:15px;transition:.2s;cursor:pointer;} .np-switch-slider:before{content:"";position:absolute;width:24px;height:24px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s;box-shadow:0 1px 3px rgba(0,0,0,.2);} .np-switch input:checked+.np-switch-slider{background:#059669;} .np-switch input:checked+.np-switch-slider:before{transform:translateX(22px);} #npOverlayBadge{position:fixed;left:10px;bottom:calc(18px + env(safe-area-inset-bottom));z-index:2147483645;background:#059669;color:#fff;font-size:11px;padding:6px 10px;border-radius:8px;display:none;max-width:42vw;line-height:1.3;box-shadow:0 2px 8px rgba(0,0,0,.25);} `; const root = document.createElement('div'); root.id = 'npRenameRoot'; root.innerHTML = `

    NAMCO Parks 改个人信息

    需已登录 parks2。改的是会员资料/会員情報変更中的姓名、生日与性别,无 SMS(手机号不变)。

    ⚠ 「提交修改」改服务器会员资料(姓名/生日/性别)。官网编辑页生日/性别虽显示只读,接口可改。「券面强制显示」仅本机浏览器覆盖画面。
    券面强制显示

    开启后替换/插入券面姓名。iPhone 使用済み券有时官方不显示姓名,开此开关并填写姓名即可补上;刷新后仍有效。

    隐藏插件按钮

    隐藏后连点屏幕右下角两次可再打开设置

    请先登录 NAMCO,再点「读取当前」。
    `; document.documentElement.appendChild(root); const fab = $('#npRenameFab', root); const mask = $('#npRenameMask', root); const panel = $('#npRenamePanel', root); const logEl = $('#npLog', root); const overlayBadge = $('#npOverlayBadge', root); function log(msg) { logEl.textContent = msg; } function refreshOverlayBadge() { if (shouldHidePluginUi()) { overlayBadge.style.display = 'none'; return; } const cfg = getOverlayConfig(); if (cfg.enabled && cfg.displayName) { overlayBadge.style.display = 'block'; overlayBadge.textContent = '券面强制显示:' + cfg.displayName; } else { overlayBadge.style.display = 'none'; } } function refreshPluginUiVisibility() { fab.style.display = shouldHidePluginUi() ? 'none' : ''; refreshOverlayBadge(); } function loadHideUiToUI() { $('#npHideUi', root).checked = shouldHidePluginUi(); } function syncOverlayFromForm() { const name = buildDisplayName( $('#npL', root).value.trim(), $('#npF', root).value.trim() ); if (name) $('#npOverlayName', root).value = name; return name; } function saveOverlayFromUI() { const enabled = $('#npOverlayOn', root).checked; const displayName = ($('#npOverlayName', root).value || syncOverlayFromForm()).trim(); setOverlayConfig({ enabled, displayName }); refreshPluginUiVisibility(); if (enabled && displayName) { findTicketNameNodes(document, true).forEach((el) => { el.dataset.npOverlay = '1'; }); const n = applyTicketOverlay(true); return { enabled, displayName, patched: n }; } return { enabled, displayName, patched: 0 }; } function loadOverlayToUI() { const cfg = getOverlayConfig(); $('#npOverlayOn', root).checked = !!cfg.enabled; if (cfg.displayName) $('#npOverlayName', root).value = cfg.displayName; loadHideUiToUI(); refreshPluginUiVisibility(); } function clearAllInputs() { $('#npPaste', root).value = ''; $('#npL', root).value = ''; $('#npF', root).value = ''; $('#npLk', root).value = ''; $('#npFk', root).value = ''; $('#npBirthday', root).value = ''; $('#npGender', root).value = 'M'; $('#npPwd', root).value = ''; $('#npOverlayName', root).value = ''; } function openPanel() { mask.style.display = 'block'; panel.classList.add('open'); clearAllInputs(); loadOverlayToUI(); loadHideUiToUI(); if (isLoggedInFromDom()) { log('✅ 当前页已登录\n• 手机没名字:开「券面强制显示」+ 填姓名\n• 必须在「詳細」页(有 EC 号那页),不是列表页'); } else { log('⚠ 未检测到登录(改服务器资料才需要)\n• 手机券面没名字:直接开「券面强制显示」填姓名即可'); } } function closePanel() { panel.classList.remove('open'); mask.style.display = 'none'; saveOverlayFromUI(); } fab.addEventListener('click', openPanel); mask.addEventListener('click', closePanel); $('#npRenameClose', root).addEventListener('click', closePanel); $('#npHideUi', root).addEventListener('change', () => { setHideUi({ hidden: $('#npHideUi', root).checked }); refreshPluginUiVisibility(); }); (function setupSecretOpen() { let lastTap = 0; function hitCorner(x, y) { const margin = 72; return x >= window.innerWidth - margin && y >= window.innerHeight - margin; } function onCornerTap(clientX, clientY) { if (!shouldHidePluginUi()) return; if (panel.classList.contains('open')) return; if (!hitCorner(clientX, clientY)) return; const now = Date.now(); if (now - lastTap < 450) { lastTap = 0; openPanel(); } else { lastTap = now; } } document.addEventListener( 'touchend', (e) => { const t = e.changedTouches && e.changedTouches[0]; if (t) onCornerTap(t.clientX, t.clientY); }, { passive: true } ); document.addEventListener('click', (e) => { if (e.target.closest('#npRenameRoot')) return; onCornerTap(e.clientX, e.clientY); }); })(); $('#npOverlayOn', root).addEventListener('change', () => { const r = saveOverlayFromUI(); if (r.enabled && !r.displayName) { log('请先填写「券面显示姓名」'); $('#npOverlayOn', root).checked = false; setOverlayConfig({ enabled: false, displayName: '' }); refreshPluginUiVisibility(); return; } log(r.enabled ? `✅ 券面强制显示已开启:${r.displayName}\n刷新/店员 F5 后会自动再覆盖。` : '券面强制显示已关闭'); }); $('#npOverlayName', root).addEventListener('input', () => { if ($('#npOverlayOn', root).checked) saveOverlayFromUI(); }); $('#npSyncOverlay', root).addEventListener('click', () => { const name = syncOverlayFromForm(); if (!name) { log('请先在下方填写完整姓名或姓/名'); return; } const r = saveOverlayFromUI(); log(`券面显示名:${name}${r.enabled ? '(已生效)' : '(请打开开关)'}`); }); loadOverlayToUI(); refreshPluginUiVisibility(); if (getOverlayConfig().enabled) applyTicketOverlay(true); // 快速填充逻辑:解析从 Excel 复制的整行内容(已补全平假名/片假名支持) $('#npQuickFill', root).addEventListener('click', () => { const rawText = $('#npPaste', root).value.trim(); if (!rawText) { log('请先粘贴 Excel 行数据到快速录入框'); return; } // 1. 优先按 Tab 制表符(Excel 复制的默认分隔符)或 2 个以上空格拆分 const cols = rawText.split(/\t+|\s{2,}/).map(c => c.trim()).filter(Boolean); const tokens = cols.length > 1 ? cols : rawText.split(/\s+/).map(c => c.trim()).filter(Boolean); let nameStr = ''; let kanaStr = ''; let genderStr = ''; let bdayStr = ''; let pwdStr = ''; // 匹配平假名与片假名的正则表达式(包含长音符号 ー) const kanaRegex = /^[\u3040-\u309F\u30A0-\u30FF\u30FC\s]+$/; // 2. 智能提取字段 tokens.forEach(token => { // 匹配生日: YYYY-MM-DD / YYYY/MM/DD / 8位数字 if (!bdayStr && (/^\d{4}[-/\.]\d{1,2}[-/\.]\d{1,2}$/.test(token) || /^\d{8}$/.test(token))) { bdayStr = normalizeBirthday(token); } // 匹配性别: 男 / 女 / M / F / Male / Female else if (!genderStr && /^(男|女|M|F|Male|Female)$/i.test(token)) { genderStr = token; } // 匹配密码: 包含字母和数字组合且长度 >= 6 else if (!pwdStr && /^(?=.*[a-zA-Z])(?=.*\d).{6,}$/.test(token)) { pwdStr = token; } // 匹配平假名/片假名 else if (!kanaStr && kanaRegex.test(token)) { kanaStr = token; } // 剩余非纯数字文本作为汉字/英文姓名候选 else if (!nameStr && !/^\d+$/.test(token)) { nameStr = token; } }); if (!nameStr && tokens.length > 0) nameStr = tokens[0]; // 3. 拆分汉字/英文 姓与名 const { l, f } = splitFullName(nameStr); $('#npL', root).value = l; $('#npF', root).value = f; // 4. 拆分假名 姓与名 并填充 if (kanaStr) { const { l: lk, f: fk } = splitFullName(kanaStr); $('#npLk', root).value = lk; $('#npFk', root).value = fk; } // 5. 填充性别 if (genderStr) { if (/^(女|F|Female)$/i.test(genderStr)) { $('#npGender', root).value = 'F'; } else if (/^(男|M|Male)$/i.test(genderStr)) { $('#npGender', root).value = 'M'; } } // 6. 填充生日 if (bdayStr) { $('#npBirthday', root).value = bdayStr; } // 7. 填充密码 if (pwdStr) { $('#npPwd', root).value = pwdStr; } // 8. 同步填充券面显示姓名 const fullName = `${l} ${f}`.trim(); if (fullName) { $('#npOverlayName', root).value = fullName; } const lkVal = $('#npLk', root).value; const fkVal = $('#npFk', root).value; log(`✅ 快速填充完成:\n• 姓名:${l} ${f}\n• 假名:${lkVal || fkVal ? `${lkVal} ${fkVal}` : '未匹配'}\n• 性别:${$('#npGender', root).value === 'F' ? '女 (F)' : '男 (M)'}\n• 生日:${bdayStr || '未匹配'}\n• 密码:${pwdStr ? '已自动填充' : '未匹配'}`); }); $('#npLoad', root).addEventListener('click', async () => { log('读取中…'); try { const ok = await checkLoggedIn(); if (!ok) throw new Error('未登录,请打开网站先登录'); const p = await loadProfile(); const sexLabel = p.gender === 'F' ? '女 (F)' : '男 (M)'; log( `当前会员\n氏名:${p.last_name} ${p.first_name}\nカナ:${p.last_name_kana} ${p.first_name_kana}\n生日:${p.birthday}\n性别:${sexLabel}\n手机:${p.tel}\n邮箱:${p.email}` ); $('#npL', root).value = p.last_name || ''; $('#npF', root).value = p.first_name || ''; if (!$('#npLk', root).value) $('#npLk', root).value = p.last_name_kana || ''; if (!$('#npFk', root).value) $('#npFk', root).value = p.first_name_kana || ''; $('#npBirthday', root).value = normalizeBirthday(p.birthday); $('#npGender', root).value = p.gender || 'M'; } catch (e) { log('❌ ' + e.message); } }); $('#npSubmit', root).addEventListener('click', async () => { const l = $('#npL', root).value.trim(); const f = $('#npF', root).value.trim(); const bdayRaw = $('#npBirthday', root).value.trim(); const pwd = $('#npPwd', root).value; const gender = $('#npGender', root).value; if (!l || !f) { log('请填写姓和名'); return; } if (bdayRaw && !normalizeBirthday(bdayRaw)) { log('生日格式无效,请用 YYYY-MM-DD'); return; } if (!pwd) { log('请填写账号密码'); return; } log('提交中…请勿关页面'); try { const profile = await loadProfile(); const changes = { last_name: l, first_name: f, nickname: l, gender, }; const lk = $('#npLk', root).value.trim(); const fk = $('#npFk', root).value.trim(); if (lk) changes.last_name_kana = lk; if (fk) changes.first_name_kana = fk; const bday = normalizeBirthday(bdayRaw); if (bday) changes.birthday = bday; await updateMemberName(profile, changes, pwd); const after = await loadProfile(); const sexLabel = after.gender === 'F' ? '女 (F)' : '男 (M)'; log( `✅ 会员资料已更新\n` + `新氏名:${after.last_name} ${after.first_name}\n` + `カナ:${after.last_name_kana} ${after.first_name_kana}\n` + `生日:${after.birthday}\n` + `性别:${sexLabel}\n` + `建议开启「券面强制显示」并验证券面。` ); } catch (e) { log('❌ ' + e.message); } }); $('#npVerify', root).addEventListener('click', async () => { log('验证中…'); try { const v = await verifyTicketNames(); const prof = await loadProfile(); const sexLabel = prof.gender === 'F' ? '女 (F)' : '男 (M)'; let msg = `会员资料:${v.member}\n片假名:${v.kana || '(空)'}\n生日:${prof.birthday || '(空)'}\n性别:${sexLabel}\n`; if (v.clients.length) msg += `订单ご依頼主:${v.clients[0]}\n`; if (!v.tickets.length) { msg += '当前无入場チケット。'; } else { v.tickets.forEach((t) => { const ok = t.name === v.member; msg += `\n券面 [${t.ec}]:${t.name} ${ok ? '✅与会员一致' : '❌仍为订单快照'}`; }); } log(msg); } catch (e) { log('❌ ' + e.message); } }); })();