// ==UserScript==
// @name Amazon keywords Positioning by Asin
// @namespace http://tampermonkey.net/
// @version 2.3.1
// @description 1.在亚马逊搜索结果页上定位ASIN, 获取排名 2.代码重构————dom操作->fetch+DOMParser 3.结果面板
// @author You
// @match https://www.amazon.com/*
// @match https://www.amazon.co.uk/*
// @match https://www.amazon.ca/*
// @icon https://www.amazon.com/favicon.ico
// @license MIT
// @grant none
// ==/UserScript==
(function () {
'use strict';
// —— 配置区 ——
const DEFAULT_MAX_PAGES = 2; // 默认最多搜索页数
const STYLE = `
/* 容器 */
#tm-asin-container {
position: fixed;
top: 60px;
left: 0; right: 0;
padding: 6px 12px;
background: #fff;
padding: 6px 12px
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
font-family: "Helvetica Neue", Arial, sans-serif;
z-index: 9999;
display: flex;
align-items: center;
}
/* tag-wrapper-css */
#tm-asin-container #tag-wrapper {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-right: 6px;
}
.tag-item {
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 8px;
font-size: 14px;
background: #ecf5ff;
color: #409eff;
border: 1px solid #b3d8ff;
border-radius: 4px;
}
.tag-item .tag-close {
display: inline-block;
margin-left: 4px;
font-style: normal;
cursor: pointer;
color: #409eff;
font-weight: bold;
}
.tag-item .tag-close:hover {
color: #66b1ff;
}
.tag-add-btn {
display: inline-flex;
align-items: center;
height: 32px;
padding: 0 12px;
font-size: 14px;
color: #409eff;
background: #fff;
border: 1px solid #409eff;
border-radius: 4px;
cursor: pointer;
transition: background-color .2s;
}
.tag-add-btn:hover {
background-color: #ecf5ff;
}
/* 临时输入框 */
.tag-input {
flex: 1;
min-width: 100px;
height: 28px;
padding: 0 6px;
font-size: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
outline: none;
}
/* input错误提示 */
.input-error {
border-color: red;
outline: none;
box-shadow: 0 0 5px red;
}
/* ASIN 和页数输入框 */
#tm-asin-container input[type="number"] {
margin-right: 14px;
font-size: 16px;
border: 1px solid #dcdfe6;
border-radius: 4px;
color: #606266;
outline: none;
transition: border-color .2s, box-shadow .2s;
width: 200px;
box-sizing: border-box;
}
#tm-asin-container input:focus {
border-color: #409eff;
box-shadow: 0 0 2px rgba(64,158,255,0.2);
}
/* 按钮 */
#tm-asin-container button {
margin-right: 12px;
padding: 5px 10px;
font-size: 14px;
font-weight: 600;
color: #fff;
background-color: #409eff;
border: 1px solid #409eff;
border-radius: 4px;
cursor: pointer;
transition: background-color .2s, border-color .2s;
}
#tm-asin-container button[disabled] {
background-color: #c0c4cc;
border-color: #c0c4cc;
cursor: not-allowed;
}
#tm-asin-container button:hover:not([disabled]) {
background-color: #66b1ff;
border-color: #66b1ff;
}
#tm-asin-container span {
font-size: 16px;
}
/* 状态文字:紧跟按钮后面 */
#tm-asin-container span#tm-status {
margin-left: 12px;
font-size: 16px;
color:rgb(110, 111, 111);
}
`;
// —— 状态 ——
let targetASIN = '';
let maxPages = DEFAULT_MAX_PAGES;
// —— 注入样式 & UI ——
const styleEl = document.createElement('style');
styleEl.textContent = STYLE;
document.head.appendChild(styleEl);
// container框
const container = document.createElement('div');
container.id = 'tm-asin-container';
// tag-wrapper-1
const tagWrapper = document.createElement('div');
tagWrapper.id = 'tag-wrapper'
container.insertBefore(tagWrapper, container.firstChild);
// Max🔎Pages
const maxPageText = document.createElement('span');
maxPageText.textContent = 'Max🔎Pages:';
// maxpage input
const inputPages = document.createElement('input');
inputPages.type = 'number';
inputPages.min = '1';
inputPages.value = DEFAULT_MAX_PAGES;
inputPages.style.width = '60px';
// search button
const btnSearch = document.createElement('button');
btnSearch.textContent = '搜索排名';
// result goto
const btnGoto = document.createElement('button');
btnGoto.textContent = '跳转到结果页';
btnGoto.disabled = true;
// status的div的元素
const status = document.createElement('span');
status.setAttribute("id", "tm-status");
status.textContent = '请填写 ASIN,点击“搜索排名”';
/* 动画过渡——container栏的伸缩 */
container.style.transition = 'top 0.4s ease';
// scroll事件-待优化
let isScrolling;
let lastScrollY = window.scrollY;
window.addEventListener("scroll", () => {
// 清除之前的计时器,避免频繁触发
window.cancelAnimationFrame(isScrolling);
// 用 requestAnimationFrame 优化性能
isScrolling = window.requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
const isScrollingDown = currentScrollY > lastScrollY;
container.style.top = isScrollingDown ? "0px" : "55px";
lastScrollY = currentScrollY;
});
});
//tag-wrapper-2 初始化数据
let tagAsins = []
const maxTags = 3
// tag-wrapper-3 渲染
function renderTags() {
tagWrapper.innerHTML = '';
// 渲染每个 tag
tagAsins.forEach((tag, idx) => {
const span = document.createElement('span');
span.className = 'tag-item';
span.textContent = tag;
// close按钮
const close = document.createElement('i');
close.className = 'tag-close';
close.textContent = '×';
close.addEventListener('click', () => {
tagAsins.splice(idx, 1);
renderTags();
// 可在此触发“close”事件回调
});
span.appendChild(close);
tagWrapper.appendChild(span);
});
// 渲染"+ New Asin"按钮
const btnAdd = document.createElement('button');
btnAdd.className = 'tag-add-btn';
btnAdd.textContent = '+ New Asin';
btnAdd.addEventListener('click', showInput);
tagWrapper.appendChild(btnAdd);
}
// tag-wrapper-4 显示输入框新增
function showInput() {
// 如果已经有输入框,直接聚焦
const existingInput = tagWrapper.querySelector('input.tag-input');
if (existingInput) {
existingInput.focus();
return;
}
const input = document.createElement('input');
input.className = 'tag-input';
input.placeholder = 'Enter ASIN';
// Asin检验格式
const asinRegex = /^B0[A-Z0-9]{8}$/;
// 插到按钮前
tagWrapper.insertBefore(input, tagWrapper.querySelector('.tag-add-btn'));
input.focus();
// 只保留一个 confirmInput,接收事件对象
function confirmInput(e) {
const v = input.value.trim().replace(/,$/, '');
// —— 1. 如果是 blur 触发,只处理“空值移除”或“合法新值添加”
if (e.type === 'blur') {
if (!v) {
input.remove();
renderTags();
} else if (!tagAsins.includes(v) && tagAsins.length < maxTags && /^B0[A-Z0-9]{8}$/.test(v)) {
tagAsins.push(v);
input.remove();
renderTags();
}
// 其它情况(重复/不合法/超限)都不 alert,也不移除,让用户继续改
return;
}
// —— 2. 如果是 keydown 且回车,做完整校验
if (e.type === 'keydown' && e.key === 'Enter') {
e.preventDefault();
// 空值 —— 直接移除
if (!v) {
input.remove();
renderTags();
return;
}
// 格式不对
if (!/^B0[A-Z0-9]{8}$/.test(v)) {
input.classList.add('input-error');
alert(`ASIN "${v}" 格式不正确!`);
input.focus();
return;
}
// 重复
if (tagAsins.includes(v)) {
input.classList.add('input-error');
alert(`ASIN "${v}" 已存在!`);
input.focus();
return;
}
// 超限
if (tagAsins.length >= maxTags) {
input.classList.add('input-error');
alert(`最多只能添加 ${maxTags} 个 ASIN!`);
input.focus();
return;
}
// 全部通过——添加并移除
tagAsins.push(v);
input.remove();
renderTags();
}
}
input.addEventListener('keydown', confirmInput);
input.addEventListener('blur', confirmInput);
}
// tag-wrapper-5 初次渲染
renderTags();
[maxPageText, inputPages, btnSearch, btnGoto, status].forEach(el => container.appendChild(el));
document.body.appendChild(container);
// —— 状态更新 ——
const updateStatus = txt => { status.textContent = txt; };
// —— 主搜索逻辑 ——
btnSearch.addEventListener('click', async () => {
// search-1 参数
maxPages = parseInt(inputPages.value, 10) || DEFAULT_MAX_PAGES;
btnGoto.disabled = true;
if (!tagAsins.length) return alert('请先添加至少一个 ASIN!');
// search-2 初始化结果存储
const results = {};
tagAsins.forEach(a => results[a] = { found: false });
// search-3 删除原有 page 参数
const baseUrl = new URL(location.href);
baseUrl.searchParams.delete('page');
// search-4 顺序翻页
updateStatus(`🔎 开始搜索 ${tagAsins.length}个 ASIN,最多 ${maxPages} 页......`);
for (let page = 1; page <= maxPages; page++) {
updateStatus(`🔎 正在搜索第 ${page} 页…`);
const url = new URL(baseUrl);
// 重新设置 page 参数
url.searchParams.set('page', page);
// search-4.1 拉取解析HTML
// fetch获取html字符串 DOMParser转成document对象 再搜索
let doc
try {
// 表示跨域请求时会带上 cookie(登录(不可用)态)
const resp = await fetch(url.href, { credentials: 'include' });
const html = await resp.text();
doc = new DOMParser().parseFromString(html, 'text/html');
} catch (e) {
console.error(e);
updateStatus('❌ 网络请求出错,请重试');
return;
}
// search-4.2 扫描当前页 统计所有未找到的 ASIN
const items = doc.querySelectorAll('div[data-asin]')
let nat = 0, sp = 0;
for (const node of items) {
// 带有购物车按钮的才算有效位置
if (!node.querySelector('button.a-button-text, a.a-button-text')) continue;
const asin = node.getAttribute('data-asin');
// 广告位
const isAd = !!node.querySelector('a.puis-label-popover.puis-sponsored-label-text');
isAd ? sp++ : nat++;
// 如果这个 ASIN 在 tagAsins 列表里,且还没找到,就记录它
if (tagAsins.includes(asin) && !results[asin].found) {
results[asin] = {
found: true,
page,
position: isAd ? sp : nat,
isAd
};
}
}
// search-4.3 如果所有 ASIN 都已找到,则提前退出翻页
const unfinished = tagAsins.filter(asin => !results[asin].found);
if (unfinished.length === 0) {
updateStatus(`✅ 全部 ASIN 在 ${page} 页内找到`);
break;
}
}
// search-5 更新最终状态 & (可选) 渲染结果
const notFound = tagAsins.filter(asin => !results[asin].found);
if (notFound.length) {
updateStatus(`❌ 未找到:${notFound.join(', ')}`);
} else {
updateStatus(`✅ 全部 ASIN 已定位`);
btnGoto.disabled = false;
// 跳转到最后一页查看详情
btnGoto.onclick = () => location.href =
new URL(baseUrl).searchParams.set('page', results[tagAsins[tatagAsinsgs.length - 1]].page) && baseUrl.href;
}
renderResultsPanel(results)
});
// —— 搜索结果面板 ——
function renderResultsPanel(results) {
// 如果已经有面板,复用它
let panel = document.getElementById('results-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'results-panel';
Object.assign(panel.style, {
position: 'fixed',
top: '100px', // 根据你的 tm-asin-container 高度适当调整
left: '10px',
background: 'rgba(255,255,255,0.95)',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
zIndex: '9999',
fontSize: '14px',
maxWidth: '270px',
lineHeight: '1.4'
});
document.body.appendChild(panel);
}
let html = '<h4 style="margin:0 0 8px 0;font-size:16px;">查询结果</h4>';
html += '<ul style="list-style:none;padding:0;margin:0;">';
for (const asin in results) {
const r = results[asin];
html += '<li style="margin-bottom:4px;">';
html += `<strong>${asin}</strong>:`;
if (r.found) {
html += `第${r.page}页,第${r.position}位`;
if (r.isAd) html += '(广告位)';
} else {
html += `<span style="color:#f56c6c;list-style-type: none !important;">未找到</span>`;
}
html += '</li>';
}
html += '</ul>';
panel.innerHTML = html;
// 可拖拽逻辑开始
panel.style.cursor = 'move'; // 显示拖拽手型
panel.onmousedown = function (e) {
// 计算按下点与面板左上角的偏移
const rect = panel.getBoundingClientRect();
const shiftX = e.clientX - rect.left;
const shiftY = e.clientY - rect.top;
function onMouseMove(e) {
panel.style.left = (e.clientX - shiftX) + 'px';
panel.style.top = (e.clientY - shiftY) + 'px';
}
document.addEventListener('mousemove', onMouseMove);
document.onmouseup = function () {
document.removeEventListener('mousemove', onMouseMove);
document.onmouseup = null;
};
// 阻止默认拖拽行为
e.preventDefault();
};
panel.ondragstart = () => false;
// 可拖拽逻辑结束
}
})();