// ==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();
});
})();