// ==UserScript== // @name 车牌标注-阿拉伯字符(拼接+右键粘贴) // @namespace http://tampermonkey.net/ // @version 2.2 // @description 按点击顺序拼接车牌;自动加空格;复制后可直接右键粘贴到平台输入框;也支持拉丁键盘转阿拉伯字符 // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; // 数字:阿拉伯-印度数字;字母:埃及车牌使用的 17 个字母(按拉丁对应字母 A-Y 排序,便于键盘输入) const numArr = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; const charArr = ['أ', 'ب', 'ص', 'د', 'ع', 'ف', 'ج', 'ه', 'ق', 'ل', 'م', 'ن', 'ر', 'س', 'ط', 'و', 'ى']; const latinArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'K', 'L', 'M', 'N', 'R', 'S', 'T', 'W', 'Y']; const keyMap = {}; numArr.forEach((c, i) => { keyMap[String(i)] = c; }); charArr.forEach((c, i) => { keyMap[latinArr[i].toLowerCase()] = c; }); // ---------- 共享工具(每个 frame 都需要) ---------- function isDigitChar(ch) { return numArr.indexOf(ch) !== -1; } // 字母与字母之间加空格、字母与数字之间加空格、数字与数字之间不加空格 function formatPlate(raw) { if (!raw) return ''; let out = ''; for (let i = 0; i < raw.length; i++) { const ch = raw[i]; if (i > 0) { const prevIsDigit = isDigitChar(raw[i - 1]); const curIsDigit = isDigitChar(ch); if (!(prevIsDigit && curIsDigit)) out += ' '; } out += ch; } return out; } function isEditable(el) { if (!el) return false; const tag = (el.tagName || '').toLowerCase(); return tag === 'input' || tag === 'textarea' || el.isContentEditable; } function insertInto(el, text) { try { el.focus(); if (el.isContentEditable) { el.textContent = text; } else if (typeof el.value === 'string') { el.value = text; } else { return false; } try { el.dispatchEvent(new InputEvent('input', { bubbles: true })); } catch (e) { el.dispatchEvent(new Event('input', { bubbles: true })); } try { el.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {} return true; } catch (e) { return false; } } // 把面板里拼接好的文字同步到顶层窗口,方便子 frame 右键读取 function syncText(text) { window.__egPlateText = text; } function getSyncedText() { try { if (window !== window.top && window.top && window.top.__egPlateText != null) { return window.top.__egPlateText; } } catch (e) {} return (window.__egPlateText || ''); } function getRcPaste() { try { if (window !== window.top && window.top && window.top.__egRcPaste != null) return !!window.top.__egRcPaste; } catch (e) {} return window.__egRcPaste !== false; } // 复制提示(每个 frame 独立) let toastTimeout; function toast(msg) { let toast = document.querySelector('.eg-copy-toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'eg-copy-toast'; toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%) translateY(-20px);background:#10b981;color:#fff;padding:6px 16px;border-radius:4px;font-size:14px;z-index:2147483648;opacity:0;transition:all .3s ease;pointer-events:none;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;white-space:nowrap;'; document.body.appendChild(toast); } toast.textContent = msg; toast.classList.add('show'); clearTimeout(toastTimeout); toastTimeout = setTimeout(() => toast.classList.remove('show'), 1200); } // ---------- 右键直接粘贴:在任意输入框上右键,就把拼接好的车牌写进去 ---------- document.addEventListener('contextmenu', (e) => { if (!getRcPaste()) return; // 用户关掉了右键粘贴 const text = formatPlate(getSyncedText()); if (!text) return; // 还没拼接内容时,保留原生右键菜单 if (!isEditable(e.target)) return; // 只在输入框/文本域上生效 e.preventDefault(); e.stopPropagation(); if (insertInto(e.target, text)) toast('已右键粘贴:' + text); }, true); // ---------- 面板 UI 只在最外层窗口创建,避免 iframe 里出现多个面板 ---------- const isTop = (function () { try { return window === window.top; } catch (e) { return true; } })(); if (!isTop) return; const numHtml = numArr.map((c, i) => `
${c}${i}
` ).join(''); const letterHtml = charArr.map((c, i) => `
${c}${latinArr[i]}
` ).join(''); const panel = document.createElement('div'); panel.id = 'eg-plate-helper'; panel.tabIndex = 0; panel.innerHTML = `
🇪🇬 埃及车牌字符 🖱 拼接
复制
退格
清空
自动粘贴: 开 右键粘贴: 开
点字符按点击顺序拼到上方(不会乱序),字母间自动加空格;也可直接打字(A=أ B=ب … 0=٠)。
▸ 车牌数字
${numHtml}
▸ 车牌字母
${letterHtml}
`; const style = document.createElement('style'); style.textContent = ` @font-face { font-family: "KacstOne"; src: url('https://cdn.jsdelivr.net/npm/font-kacstone@1.0.0/KacstOne.ttf') format('truetype'); font-display: swap; } #eg-plate-helper { position: fixed !important; top: 50px !important; left: 100px !important; width: 320px !important; background: #ffffff !important; border: 1px solid #e0e0e0 !important; border-radius: 8px !important; box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important; z-index: 2147483647 !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; overflow: hidden !important; user-select: none !important; pointer-events: auto !important; will-change: transform !important; outline: none !important; } #eg-plate-header { display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 8px 12px !important; background: #2563eb !important; color: white !important; font-size: 13px !important; font-weight: bold !important; cursor: move !important; } .eg-title { pointer-events: none !important; } .eg-header-actions { display: flex !important; gap: 8px !important; align-items: center !important; } #eg-plate-mode { cursor: pointer !important; font-size: 12px !important; font-weight: normal !important; background: rgba(255,255,255,0.18) !important; border-radius: 4px !important; padding: 2px 6px !important; } #eg-plate-mode:hover { background: rgba(255,255,255,0.32) !important; } #eg-plate-toggle { cursor: pointer !important; padding: 0 4px !important; font-size: 16px !important; line-height: 1 !important; pointer-events: auto !important; } #eg-plate-toggle:hover { opacity: 0.8 !important; } #eg-plate-body { padding: 8px !important; max-height: 460px !important; overflow-y: auto !important; background: white !important; pointer-events: auto !important; } #eg-plate-body.hidden { display: none !important; } .eg-compose { padding: 2px 2px 6px !important; } #eg-compose-box { display: flex !important; flex-wrap: wrap !important; align-items: center !important; gap: 4px !important; direction: ltr !important; min-height: 38px !important; padding: 6px 8px !important; border: 1px solid #d1d5db !important; border-radius: 6px !important; background: #f9fafb !important; } #eg-compose-box.empty::before { content: "点击下方字符开始拼接"; font-size: 14px !important; color: #9ca3af !important; } .eg-token { display: inline-flex !important; align-items: center !important; justify-content: center !important; min-width: 26px !important; height: 32px !important; padding: 0 3px !important; background: #eef2ff !important; border: 1px solid #c7d2fe !important; border-radius: 5px !important; font-family: "KacstOne", "Segoe UI", sans-serif !important; font-size: 21px !important; line-height: 1 !important; color: #1e3a8a !important; } .eg-token-sm { min-width: 20px !important; height: 26px !important; font-size: 17px !important; border-radius: 4px !important; } .eg-compose-actions { display: flex !important; gap: 6px !important; margin: 6px 0 4px !important; } .eg-tool-btn { flex: 1 !important; text-align: center !important; padding: 5px 0 !important; border-radius: 6px !important; border: 1px solid #d1d5db !important; background: #ffffff !important; color: #374151 !important; font-size: 13px !important; cursor: pointer !important; user-select: none !important; pointer-events: auto !important; } .eg-tool-btn:hover { border-color: #2563eb !important; color: #2563eb !important; } .eg-tool-btn.eg-primary { background: #2563eb !important; border-color: #2563eb !important; color: white !important; } .eg-tool-btn.eg-primary:hover { background: #1d4ed8 !important; } .eg-opts { display: flex !important; gap: 10px !important; align-items: center !important; } .eg-opt { font-size: 12px !important; color: #2563eb !important; cursor: pointer !important; text-decoration: underline dotted !important; } .eg-opt.off { color: #9ca3af !important; } .eg-hint { font-size: 11px !important; color: #888 !important; line-height: 1.5 !important; margin-top: 6px !important; } .group-tip { font-size: 12px; color: #666; margin: 6px 0 4px; } .wrap-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } .eg-char-btn { width: 44px !important; height: 52px !important; display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 1px !important; font-family: "KacstOne", sans-serif !important; background: #f8f9fa !important; border: 1px solid #dee2e6 !important; border-radius: 6px !important; cursor: pointer !important; transition: all 0.15s ease !important; color: black !important; user-select: none !important; pointer-events: auto !important; } .eg-char-btn .eg-ar { font-size: 23px !important; line-height: 1 !important; } .eg-char-btn .eg-lat { font-size: 10px !important; color: #999 !important; line-height: 1 !important; font-family: 'Segoe UI', sans-serif !important; } .eg-char-btn:hover { background: #eff6ff !important; border-color: #2563eb !important; color: #2563eb !important; transform: translateY(-2px) !important; } .eg-char-btn:hover .eg-lat { color: #2563eb !important; } .eg-char-btn:active { transform: translateY(0) !important; } .eg-recent { margin-top: 4px !important; border-top: 1px dashed #e5e7eb !important; padding-top: 6px !important; } .eg-recent-item { display: inline-flex !important; gap: 3px !important; padding: 3px 6px !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; background: #fff !important; cursor: pointer !important; user-select: none !important; pointer-events: auto !important; } .eg-recent-item:hover { border-color: #2563eb !important; } .eg-recent-item:hover .eg-token { border-color: #2563eb !important; color: #2563eb !important; background: #eff6ff !important; } .eg-copy-toast { position: fixed !important; top: 20px !important; left: 50% !important; transform: translateX(-50%) translateY(-20px) !important; background: #10b981 !important; color: white !important; padding: 6px 16px !important; border-radius: 4px !important; font-size: 14px !important; z-index: 2147483648 !important; opacity: 0 !important; transition: all 0.3s ease !important; pointer-events: none !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; white-space: nowrap !important; } .eg-copy-toast.show { opacity: 1 !important; transform: translateX(-50%) translateY(0) !important; } `; document.head.appendChild(style); document.body.appendChild(panel); const bodyEl = document.getElementById('eg-plate-body'); const composeBox = document.getElementById('eg-compose-box'); const copyAllBtn = document.getElementById('eg-copy-all'); const backspaceBtn = document.getElementById('eg-backspace'); const clearBtn = document.getElementById('eg-clear'); const modeBtn = document.getElementById('eg-plate-mode'); const toggleBtn = document.getElementById('eg-plate-toggle'); const recentEl = document.getElementById('eg-recent'); const autoPasteBtn = document.getElementById('eg-autopaste'); const rcPasteBtn = document.getElementById('eg-rcpaste'); let composeStr = ''; let singleClick = false; let autoPaste = true; // 复制/回车时自动写入之前点过的输入框 let lastField = null; function tokenHtml(str, extraClass) { if (!str) return ''; const cls = 'eg-token ' + (extraClass || ''); return [...str].map((ch) => `${ch}`).join(''); } function updateCompose() { composeBox.innerHTML = composeStr ? tokenHtml(composeStr) : ''; composeBox.classList.toggle('empty', !composeStr); syncText(composeStr); // 同步到顶层,供右键读取 } function updateModeBtn() { modeBtn.textContent = singleClick ? '🖱 单字' : '🖱 拼接'; modeBtn.title = singleClick ? '当前:点击字符只复制单个字符' : '当前:点击字符会拼接并复制整段'; } function updateAutoPaste() { autoPasteBtn.textContent = autoPaste ? '自动粘贴: 开' : '自动粘贴: 关'; autoPasteBtn.classList.toggle('off', !autoPaste); autoPasteBtn.title = autoPaste ? '复制/回车时自动写入之前点过的输入框' : '只复制到剪贴板,手动右键粘贴'; } function updateRcPaste() { const on = window.__egRcPaste !== false; rcPasteBtn.textContent = on ? '右键粘贴: 开' : '右键粘贴: 关'; rcPasteBtn.classList.toggle('off', !on); rcPasteBtn.title = on ? '在平台输入框上右键就直接写入车牌' : '关闭右键粘贴,右键走原生菜单'; } async function copyText(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { const textarea = document.createElement('textarea'); textarea.value = text; textarea.setAttribute('readonly', ''); textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; textarea.style.top = '0'; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); let ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } document.body.removeChild(textarea); return ok; } } async function copyChar(text) { const ok = await copyText(text); toast(ok ? `已复制: ${text}` : '复制失败'); } // 只复制整段到剪贴板(每点一个字符都会同步更新,方便随时右键粘贴) async function copyComposed() { if (!composeStr) { toast('内容为空'); return; } const text = formatPlate(composeStr); const ok = await copyText(text); toast(ok ? `已复制整段: ${text}` : '复制失败'); } // 复制 + (可选)自动写入目标框 + 记住最近车牌 async function commitComposed() { if (!composeStr) { toast('内容为空'); return; } const text = formatPlate(composeStr); const ok = await copyText(text); remember(composeStr); let pasted = false; if (autoPaste && lastField) pasted = insertInto(lastField, text); if (pasted) toast('已复制并粘贴到目标框'); else toast(ok ? `已复制整段: ${text}` : '复制失败'); } // 点击字符 bodyEl.addEventListener('click', (e) => { const btn = e.target.closest('.eg-char-btn'); if (!btn) return; const ch = btn.dataset.char; if (singleClick) { copyChar(ch); } else { composeStr += ch; updateCompose(); copyComposed(); } }); // 工具按钮 copyAllBtn.addEventListener('click', commitComposed); backspaceBtn.addEventListener('click', () => { composeStr = composeStr.slice(0, -1); updateCompose(); if (composeStr) copyComposed(); else toast('已清空'); }); clearBtn.addEventListener('click', () => { composeStr = ''; updateCompose(); toast('已清空'); }); modeBtn.addEventListener('click', () => { singleClick = !singleClick; updateModeBtn(); }); autoPasteBtn.addEventListener('click', () => { autoPaste = !autoPaste; updateAutoPaste(); }); rcPasteBtn.addEventListener('click', () => { window.__egRcPaste = !(window.__egRcPaste !== false); updateRcPaste(); }); updateModeBtn(); updateAutoPaste(); updateRcPaste(); // 记录用户最后点击的输入框(忽略面板自身),供「复制」按钮自动粘贴用 document.addEventListener('focusin', (e) => { const t = e.target; if (!t) return; if (t === panel || panel.contains(t)) return; if (isEditable(t)) lastField = t; }); // 键盘输入(拼接模式):在面板上打字会自动转成阿拉伯字符 panel.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey || e.altKey) return; const k = e.key; if (k === 'Backspace') { e.preventDefault(); composeStr = composeStr.slice(0, -1); updateCompose(); if (composeStr) copyComposed(); else toast('已清空'); return; } if (k === 'Enter') { e.preventDefault(); commitComposed(); return; } if (k === 'Escape') { e.preventDefault(); composeStr = ''; updateCompose(); toast('已清空'); return; } const mapped = keyMap[k.toLowerCase()]; if (mapped) { e.preventDefault(); composeStr += mapped; updateCompose(); copyComposed(); } }); panel.addEventListener('mousedown', () => { panel.focus({ preventScroll: true }); }); // 折叠 toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); if (bodyEl.classList.contains('hidden')) { bodyEl.classList.remove('hidden'); toggleBtn.textContent = '—'; } else { bodyEl.classList.add('hidden'); toggleBtn.textContent = '+'; } }); // 最近车牌 const STORAGE_KEY = 'eg-plate-recent'; let recentList = []; try { recentList = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch (e) { recentList = []; } function remember(text) { if (!text) return; recentList = [text, ...recentList.filter((x) => x !== text)].slice(0, 5); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(recentList)); } catch (e) {} renderRecent(); } function renderRecent() { recentEl.innerHTML = ''; if (!recentList.length) return; const tip = document.createElement('div'); tip.className = 'group-tip'; tip.textContent = '▸ 最近车牌(点击载入)'; recentEl.appendChild(tip); const row = document.createElement('div'); row.className = 'wrap-row'; recentList.forEach((text) => { const chip = document.createElement('div'); chip.className = 'eg-recent-item'; chip.title = '点击载入'; chip.innerHTML = tokenHtml(text, 'eg-token-sm'); chip.addEventListener('click', () => { composeStr = text; updateCompose(); commitComposed(); }); row.appendChild(chip); }); recentEl.appendChild(row); } renderRecent(); // 拖拽 let isDragging = false; let dragStartX = 0, dragStartY = 0; let initialTranslateX = 0, initialTranslateY = 0; let hasMoved = false; const header = document.getElementById('eg-plate-header'); function getCurrentTransform() { const transform = window.getComputedStyle(panel).transform; if (transform === 'none') return { x: 0, y: 0 }; const matrix = transform.match(/matrix\(([^)]+)\)/); if (matrix) { const values = matrix[1].split(', '); return { x: parseFloat(values[4]) || 0, y: parseFloat(values[5]) || 0 }; } return { x: 0, y: 0 }; } function onMouseMove(e) { if (!isDragging) return; e.preventDefault(); const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; panel.style.transform = `translate(${initialTranslateX + dx}px, ${initialTranslateY + dy}px)`; panel.style.top = panel.style.left = panel.style.right = panel.style.bottom = 'auto'; hasMoved = true; } function onMouseUp() { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); document.body.style.userSelect = ''; setTimeout(() => { hasMoved = false; }, 10); } function onMouseDown(e) { if (e.target === toggleBtn || toggleBtn.contains(e.target)) return; if (e.target === modeBtn || modeBtn.contains(e.target)) return; if (e.target === autoPasteBtn || autoPasteBtn.contains(e.target)) return; if (e.target === rcPasteBtn || rcPasteBtn.contains(e.target)) return; e.preventDefault(); isDragging = true; hasMoved = false; dragStartX = e.clientX; dragStartY = e.clientY; const transform = getCurrentTransform(); initialTranslateX = transform.x; initialTranslateY = transform.y; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } header.addEventListener('mousedown', onMouseDown); panel.addEventListener('click', (e) => { if (hasMoved) e.stopPropagation(); }); })();