// ==UserScript==
// @name 全国统一规范电子税务局税号填写辅助工具
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 在电子税务局旁边显示一个小窗口,方便登录不同的公司,支持开关控制是否自动填写账号密码
// @author Herohub
// @match https://*.chinatax.gov.cn:8443/*
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
'use strict';
// 判断是否为登录页面,若不是则直接退出脚本执行
const isLoginPage = window.location.href.includes('login?redirect_uri');
if (!isLoginPage) return;
// 创建浮窗元素并设置样式
const createFloatWindow = () => {
const floatWindow = document.createElement('div');
Object.assign(floatWindow.style, {
position: 'fixed',
top: '50%',
right: '20px',
transform: 'translateY(-50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '15px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
fontFamily: 'Arial,sans-serif',
zIndex: 10000
});
return floatWindow;
};
const floatWindow = createFloatWindow();
document.body.appendChild(floatWindow);
// 创建按钮元素并设置样式和点击事件
const createButton = (text, clickHandler, isDelete = false) => {
const button = document.createElement('button');
Object.assign(button.style, {
marginRight: '5px',
padding: '5px 10px',
border: 'none',
borderRadius: '3px',
background: isDelete? '#dc3545' : '#007BFF',
color: '#fff',
cursor: 'pointer',
transition: 'background-color 0.3s'
});
button.textContent = text;
button.addEventListener('click', clickHandler);
// 鼠标悬停效果
button.addEventListener('mouseover', () => button.style.background = isDelete? '#c82333' : '#0056b3');
button.addEventListener('mouseout', () => button.style.background = isDelete? '#dc3545' : '#007BFF');
return button;
};
// 定义全局变量
let isEditMode = false;
let lastSelectedItem = GM_getValue('lastSelectedItem', null);
let originalWidth = null;
let isAutoFillAccount = GM_getValue('isAutoFillAccount', false);
let accountInfo = GM_getValue('accountInfo', null);
// 添加按钮,点击触发添加新条目功能
const addButton = createButton('添加', addItem);
floatWindow.appendChild(addButton);
// 编辑/完成按钮,点击切换编辑模式
let editButton = createButton('编辑', toggleEditMode);
floatWindow.appendChild(editButton);
// 导入按钮,初始隐藏,用于导入数据
const importButton = createButton('导入', importData);
importButton.style.display = 'none';
floatWindow.appendChild(importButton);
// 导出按钮,初始隐藏,用于导出数据
const exportButton = createButton('导出', exportData);
exportButton.style.display = 'none';
floatWindow.appendChild(exportButton);
// 账号自动填写开关按钮,点击切换自动填充状态
const accountAutoFillSwitch = createButton(isAutoFillAccount? '自动填账号密码: 开' : '自动填账号密码: 关', toggleAccountAutoFill);
floatWindow.appendChild(accountAutoFillSwitch);
// 分割线,用于区分按钮和条目列表
const separator = document.createElement('hr');
separator.style.cssText = 'margin:15px 0;border:none;border - top:1px solid #e0e0e0';
floatWindow.appendChild(separator);
// 内容列表容器,用于显示税号条目
const itemList = document.createElement('div');
floatWindow.appendChild(itemList);
// 加载已保存的税号内容
const savedItems = GM_getValue('autoFillItems', []);
savedItems.forEach(item => addItemToWindow(item.content, item.note, itemList));
// 添加新条目函数,点击添加按钮时调用
function addItem() {
const modal = createAddModal();
document.body.appendChild(modal);
}
// 创建添加新条目模态框
function createAddModal() {
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
const taxNumberInput = document.createElement('input');
taxNumberInput.placeholder = '请输入税号';
taxNumberInput.style.padding = '8px';
taxNumberInput.style.border = '1px solid #ccc';
taxNumberInput.style.borderRadius = '3px';
modal.appendChild(taxNumberInput);
const noteInput = document.createElement('input');
noteInput.placeholder = '请输入备注';
noteInput.style.padding = '8px';
noteInput.style.border = '1px solid #ccc';
noteInput.style.borderRadius = '3px';
modal.appendChild(noteInput);
const confirmButton = createButton('确认', () => {
const taxNumber = taxNumberInput.value.trim();
const note = noteInput.value.trim();
if (!taxNumber ||!note) {
alert('税号和备注都不能为空,请重新输入。');
return;
}
addItemToWindow(taxNumber, note, itemList);
const items = GM_getValue('autoFillItems', []);
items.push({ content: taxNumber, note });
GM_setValue('autoFillItems', items);
adjustWindowWidth();
document.body.removeChild(modal);
});
modal.appendChild(confirmButton);
const cancelButton = createButton('取消', () => {
document.body.removeChild(modal);
}, true);
modal.appendChild(cancelButton);
return modal;
}
// 在主文档和 iframe 中查找输入框
function findInputInIframes(selector) {
const iframes = document.querySelectorAll('iframe');
for (let i = 0; i < iframes.length; i++) {
try {
const input = iframes[i].contentDocument.querySelector(selector);
if (input) {
return input;
}
} catch (e) {
// 处理跨域访问 iframe 的情况,跳过当前 iframe 继续查找
continue;
}
}
// 若在所有 iframe 中都未找到,在主文档中查找
return document.querySelector(selector);
}
// 将新条目添加到窗口列表中
function addItemToWindow(content, note, container) {
const itemDiv = document.createElement('div');
Object.assign(itemDiv.style, {
padding: '8px 0',
cursor: 'pointer',
borderBottom: '1px solid #f0f0f0',
transition: 'background-color 0.3s',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
});
const textSpan = document.createElement('span');
textSpan.textContent = note;
itemDiv.appendChild(textSpan);
const buttonContainer = document.createElement('div');
const editBtn = createButton('编辑', () => editItem(itemDiv, content, note));
const deleteBtn = createButton('删除', () => deleteItem(itemDiv, content), true);
editBtn.style.display = 'none';
deleteBtn.style.display = 'none';
buttonContainer.appendChild(editBtn);
buttonContainer.appendChild(deleteBtn);
itemDiv.appendChild(buttonContainer);
itemDiv.addEventListener('click', () => {
// 延迟 500 毫秒执行填充操作,确保页面元素加载完成
setTimeout(() => {
const taxInput = findInputInIframes('input.el-input__inner[placeholder="统一社会信用代码/纳税人识别号"]');
if (taxInput) {
taxInput.value = content;
triggerInputEvents(taxInput);
} else {
console.error('未找到纳税人识别号输入框,请检查页面元素。');
}
if (isAutoFillAccount && accountInfo) {
const accountInput = findInputInIframes('input[placeholder="居民身份证号码/手机号码/用户名"]');
const passwordInput = findInputInIframes('input[placeholder="个人用户密码"]');
if (accountInput) {
accountInput.value = accountInfo.account;
triggerInputEvents(accountInput);
}
if (passwordInput) {
passwordInput.value = accountInfo.password;
triggerInputEvents(passwordInput);
} else {
console.error('未找到密码输入框,请检查页面元素。');
}
}
clearSelection();
itemDiv.style.background = '#e0f7fa';
lastSelectedItem = content;
GM_setValue('lastSelectedItem', lastSelectedItem);
}, 500);
});
itemDiv.addEventListener('mouseover', () => itemDiv.style.background = '#f9f9f9');
itemDiv.addEventListener('mouseout', () => {
if (itemDiv.dataset.content!== lastSelectedItem) {
itemDiv.style.background = '#fff';
}
});
itemDiv.dataset.content = content;
container.appendChild(itemDiv);
if (content === lastSelectedItem) {
itemDiv.style.background = '#e0f7fa';
}
}
// 触发输入框的 input 和 change 事件,模拟用户输入操作
function triggerInputEvents(input) {
const inputEvent = new Event('input', { bubbles: true });
const changeEvent = new Event('change', { bubbles: true });
input.dispatchEvent(inputEvent);
input.dispatchEvent(changeEvent);
}
// 清除所有条目的选中状态
function clearSelection() {
const items = itemList.querySelectorAll('div');
items.forEach(item => {
if (item.dataset.content!== lastSelectedItem) {
item.style.background = '#fff';
}
});
}
// 切换编辑模式,显示或隐藏编辑和删除按钮
function toggleEditMode() {
isEditMode =!isEditMode;
const items = itemList.querySelectorAll('div');
const showOrHideButtons = (element, show) => {
element.style.display = show? 'inline-block' : 'none';
};
items.forEach(item => {
const editBtn = item.querySelector('div button:nth-child(1)');
const deleteBtn = item.querySelector('div button:nth-child(2)');
showOrHideButtons(editBtn, isEditMode);
showOrHideButtons(deleteBtn, isEditMode);
item.style.cursor = isEditMode? 'move' : 'pointer';
item.draggable = isEditMode;
});
editButton.textContent = isEditMode? '完成' : '编辑';
showOrHideButtons(importButton, isEditMode);
showOrHideButtons(exportButton, isEditMode);
if (isEditMode) {
if (originalWidth === null) {
originalWidth = parseFloat(getComputedStyle(floatWindow).width);
}
setupDragSort();
}
adjustWindowWidth();
}
// 编辑已有条目信息
function editItem(itemDiv, oldContent, oldNote) {
const editModal = document.createElement('div');
Object.assign(editModal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
const taxNumberInput = document.createElement('input');
taxNumberInput.value = oldContent;
taxNumberInput.style.padding = '8px';
taxNumberInput.style.border = '1px solid #ccc';
taxNumberInput.style.borderRadius = '3px';
editModal.appendChild(taxNumberInput);
const noteInput = document.createElement('input');
noteInput.value = oldNote;
noteInput.style.padding = '8px';
noteInput.style.border = '1px solid #ccc';
noteInput.style.borderRadius = '3px';
editModal.appendChild(noteInput);
const confirmButton = createButton('确认', () => {
const newContent = taxNumberInput.value.trim();
const newNote = noteInput.value.trim();
if (!newContent ||!newNote) {
alert('税号和备注都不能为空,请重新输入。');
return;
}
const textSpan = itemDiv.querySelector('span');
textSpan.textContent = newNote;
itemDiv.dataset.content = newContent;
const items = GM_getValue('autoFillItems', []);
const index = items.findIndex(item => item.content === oldContent);
if (index!== -1) {
items[index] = { content: newContent, note: newNote };
GM_setValue('autoFillItems', items);
}
adjustWindowWidth();
document.body.removeChild(editModal);
});
editModal.appendChild(confirmButton);
const cancelButton = createButton('取消', () => {
document.body.removeChild(editModal);
}, true);
editModal.appendChild(cancelButton);
document.body.appendChild(editModal);
}
// 删除指定条目
function deleteItem(itemDiv, content) {
const items = GM_getValue('autoFillItems', []);
const newItems = items.filter(item => item.content!== content);
GM_setValue('autoFillItems', newItems);
itemList.removeChild(itemDiv);
if (content === lastSelectedItem) {
lastSelectedItem = null;
GM_setValue('lastSelectedItem', null);
}
adjustWindowWidth();
}
// 设置拖动排序功能,允许用户通过拖动条目改变顺序
function setupDragSort() {
let draggedItem = null;
itemList.addEventListener('dragstart', (e) => {
draggedItem = e.target;
setTimeout(() => draggedItem.style.display = 'none', 0);
});
itemList.addEventListener('dragover', (e) => {
e.preventDefault();
});
itemList.addEventListener('drop', (e) => {
e.preventDefault();
const target = e.target.closest('div');
if (target && target!== draggedItem) {
const items = Array.from(itemList.children);
const draggedIndex = items.indexOf(draggedItem);
const targetIndex = items.indexOf(target);
if (draggedIndex < targetIndex) {
itemList.insertBefore(draggedItem, target.nextSibling);
} else {
itemList.insertBefore(draggedItem, target);
}
updateItemOrder();
}
draggedItem.style.display = 'block';
draggedItem = null;
});
}
// 更新条目顺序并保存到存储中
function updateItemOrder() {
const items = Array.from(itemList.children);
const newItemOrder = items.map(item => {
const note = item.querySelector('span').textContent;
const content = item.dataset.content;
return { content, note };
});
GM_setValue('autoFillItems', newItemOrder);
}
// 调整浮窗宽度以适应内容变化
function adjustWindowWidth() {
let maxWidth = 0;
const items = itemList.querySelectorAll('span');
items.forEach(item => {
const rect = item.getBoundingClientRect();
if (rect.width > maxWidth) {
maxWidth = rect.width;
}
});
const buttonWidth = 100;
const padding = 30;
let width = maxWidth + buttonWidth + padding;
if (isEditMode) {
width *= 2;
}
floatWindow.style.width = width + 'px';
if (!isEditMode) {
originalWidth = width;
}
}
// 导入数据功能,支持用户以逗号分号及换行符格式导入税号信息
function importData() {
const importModal = document.createElement('div');
Object.assign(importModal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
const dataTextarea = document.createElement('textarea');
dataTextarea.placeholder = '请输入要导入的数据(格式:税号,备注;税号,备注;...)';
dataTextarea.style.padding = '8px';
dataTextarea.style.border = '1px solid #ccc';
dataTextarea.style.borderRadius = '3px';
importModal.appendChild(dataTextarea);
const confirmButton = createButton('确认导入', () => {
const dataStr = dataTextarea.value.trim();
if (!dataStr) {
alert('导入数据不能为空,请重新输入。');
return;
}
try {
const lines = dataStr.split(';');
const importedItems = [];
for (const line of lines) {
const [content, note] = line.split(',');
if (content && note) {
importedItems.push({ content: content.trim(), note: note.trim() });
} else {
throw new Error('导入数据格式错误,请检查。');
}
}
itemList.innerHTML = '';
GM_setValue('autoFillItems', importedItems);
importedItems.forEach(item => addItemToWindow(item.content, item.note, itemList));
adjustWindowWidth();
alert('数据导入成功!');
document.body.removeChild(importModal);
} catch (error) {
alert(`数据导入失败:${error.message}`);
}
});
importModal.appendChild(confirmButton);
const cancelButton = createButton('取消', () => {
document.body.removeChild(importModal);
}, true);
importModal.appendChild(cancelButton);
document.body.appendChild(importModal);
}
// 导出数据功能,将当前存储的税号信息以逗号分号及换行符格式导出
function exportData() {
const items = GM_getValue('autoFillItems', []);
const dataStr = items.map(item => `${item.content},${item.note}`).join(';\n');
const exportModal = document.createElement('div');
Object.assign(exportModal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
const dataTextarea = document.createElement('textarea');
dataTextarea.value = dataStr;
dataTextarea.readOnly = true;
dataTextarea.style.padding = '8px';
dataTextarea.style.border = '1px solid #ccc';
dataTextarea.style.borderRadius = '3px';
exportModal.appendChild(dataTextarea);
const copyButton = createButton('复制数据', () => {
dataTextarea.select();
document.execCommand('copy');
alert('数据已复制到剪贴板!');
});
exportModal.appendChild(copyButton);
const closeButton = createButton('关闭', () => {
document.body.removeChild(exportModal);
}, true);
exportModal.appendChild(closeButton);
document.body.appendChild(exportModal);
}
// 切换账号自动填写开关状态
function toggleAccountAutoFill() {
isAutoFillAccount =!isAutoFillAccount;
accountAutoFillSwitch.textContent = isAutoFillAccount? '自动填账号密码: 开' : '自动填账号密码: 关';
GM_setValue('isAutoFillAccount', isAutoFillAccount);
if (isAutoFillAccount) {
if (!accountInfo) {
const modal = createAccountInfoModal();
document.body.appendChild(modal);
} else {
// 延迟 500 毫秒执行填充操作,确保页面元素加载完成
setTimeout(() => {
const accountInput = findInputInIframes('input[placeholder="居民身份证号码/手机号码/用户名"]');
const passwordInput = findInputInIframes('input[placeholder="个人用户密码"]');
if (accountInput) {
accountInput.value = accountInfo.account;
triggerInputEvents(accountInput);
}
if (passwordInput) {
passwordInput.value = accountInfo.password;
triggerInputEvents(passwordInput);
}
}, 500);
}
} else {
accountInfo = null;
GM_setValue('accountInfo', null);
const accountInput = findInputInIframes('input[placeholder="居民身份证号码/手机号码/用户名"]');
const passwordInput = findInputInIframes('input[placeholder="个人用户密码"]');
if (accountInput) {
accountInput.value = '';
triggerInputEvents(accountInput);
}
if (passwordInput) {
passwordInput.value = '';
triggerInputEvents(passwordInput);
}
}
}
// 创建输入账号和密码的模态框
function createAccountInfoModal() {
const modal = document.createElement('div');
Object.assign(modal.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: '#fff',
border: '1px solid #ccc',
padding: '20px',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,.1)',
zIndex: 10001,
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
const accountInput = document.createElement('input');
accountInput.placeholder = '请输入账号';
accountInput.style.padding = '8px';
accountInput.style.border = '1px solid #ccc';
accountInput.style.borderRadius = '3px';
modal.appendChild(accountInput);
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = '请输入密码';
passwordInput.style.padding = '8px';
passwordInput.style.border = '1px solid #ccc';
passwordInput.style.borderRadius = '3px';
modal.appendChild(passwordInput);
const confirmButton = createButton('确认', () => {
const account = accountInput.value.trim();
const password = passwordInput.value.trim();
if (!account ||!password) {
alert('账号和密码都不能为空,请重新输入。');
return;
}
accountInfo = { account, password };
GM_setValue('accountInfo', accountInfo);
// 延迟 500 毫秒执行填充操作,确保页面元素加载完成
setTimeout(() => {
const accountInputOnPage = findInputInIframes('input[placeholder="居民身份证号码/手机号码/用户名"]');
const passwordInputOnPage = findInputInIframes('input[placeholder="个人用户密码"]');
if (accountInputOnPage) {
accountInputOnPage.value = account;
triggerInputEvents(accountInputOnPage);
}
if (passwordInputOnPage) {
passwordInputOnPage.value = password;
triggerInputEvents(passwordInputOnPage);
}
}, 500);
document.body.removeChild(modal);
});
modal.appendChild(confirmButton);
const cancelButton = createButton('取消', () => {
isAutoFillAccount = false;
accountAutoFillSwitch.textContent = '自动填账号密码: 关';
GM_setValue('isAutoFillAccount', isAutoFillAccount);
document.body.removeChild(modal);
}, true);
modal.appendChild(cancelButton);
return modal;
}
// 初始调整窗口宽度,以适应初始内容
adjustWindowWidth();
// 如果自动填充开关开启且已有账号信息,延迟 500 毫秒后自动填充账号和密码
if (isAutoFillAccount && accountInfo) {
setTimeout(() => {
const accountInput = findInputInIframes('input[placeholder="居民身份证号码/手机号码/用户名"]');
const passwordInput = findInputInIframes('input[placeholder="个人用户密码"]');
if (accountInput) {
accountInput.value = accountInfo.account;
triggerInputEvents(accountInput);
}
if (passwordInput) {
passwordInput.value = accountInfo.password;
triggerInputEvents(passwordInput);
}
}, 500);
}
})();