// ==UserScript==
// @name SKU填充
// @version 0.1.6
// @description 将剪切板内容拆分多行并快速填充到SKU当中哟~
// @author 鹿秋夏
// @include https://sell.publish.tmall.com/tmall/publish.htm?*
// @namespace none
// ==/UserScript==
(() => {
"use strict";
const injectSkuButton = () => {
const STYLE_PROPS = [
'width', 'height', 'margin', 'padding', 'border', 'borderRadius',
'font', 'color', 'background', 'cursor', 'boxSizing', 'lineHeight',
'display', 'alignItems', 'justifyContent', 'flexDirection'
];
const copyStyles = (s, t) => {
const style = getComputedStyle(s);
STYLE_PROPS.forEach(p => t.style[p] = style[p]);
Object.assign(t.style, {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 'normal',
verticalAlign: 'middle',
whiteSpace: 'nowrap'
});
};
const createActionHandlers = () => {
const waiter = (x, t = 5e3) => new Promise((rs, rj) => {
const check = (s = Date.now()) => setTimeout(() => {
const el = document.evaluate(x, document, null, 9).singleNodeValue;
el ? rs(el) : Date.now() - s > t ? rj(Error(`Element not found: ${x}`)) : check(s);
}, 50);
check();
});
const fillField = async (input, t) => {
input.focus();
input.select();
document.execCommand('insertText', false, t);
['input', 'change', 'blur'].forEach(e => input.dispatchEvent(new Event(e, { bubbles: 1 })));
Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value')?.set?.call(input, t);
await new Promise(r => setTimeout(r, 0));
};
return {
fillHandler: async () => {
try {
const texts = (await navigator.clipboard.readText()).split('\n').map(l => l.trim()).filter(Boolean);
for (const text of texts) {
(await waiter('//div[@id="struct-p-1627207"]//button[contains(@class, "add")]')).click();
await fillField(await waiter('//input[@placeholder="主色(必选)" and @value=""]'), text);
}
} catch(e) {
console.error('填充操作失败:', e?.message || '未知错误');
}
},
replaceHandler: async () => {
try {
const texts = (await navigator.clipboard.readText()).split('\n').map(l => l.trim()).filter(Boolean);
const xpathResult = document.evaluate(
'//input[@placeholder="主色(必选)" and @value != ""]',
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < Math.min(texts.length, xpathResult.snapshotLength); i++) {
const input = xpathResult.snapshotItem(i);
await fillField(input, texts[i]);
await new Promise(r => setTimeout(r, 50));
}
} catch(e) {
console.error('替换操作失败:', e?.message || '未知错误');
}
}
};
};
const createSkuButtons = (c, r) => {
if (c.querySelectorAll('[data-sku]').length >= 2) return;
const handlers = createActionHandlers();
const baseMarginRight = getComputedStyle(r).marginRight;
const createButton = (text, handler) => {
const b = document.createElement('button');
let pressed = false;
b.textContent = text;
b.dataset.sku = 'true';
copyStyles(r, b);
b.style.marginRight = baseMarginRight;
const states = {
base: {
backgroundColor: '#FFEBF1',
color: '#FF335E',
transition: 'all 0.3s',
minWidth: `${r.offsetWidth}px`,
flexShrink: '0',
position: 'relative'
},
hover: { background: '#FFD1E0' },
active: { background: '#FFB5C7', boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.1)' }
};
Object.assign(b.style, states.base);
const isInside = (el, { clientX: x, clientY: y }) => {
const rect = el.getBoundingClientRect();
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
};
const handleUp = e => {
if (!pressed) return;
pressed = false;
const inside = isInside(b, e);
inside && handler().catch(e => console.error(`${text}失败:`, e?.message || '未知错误'));
Object.assign(b.style, inside ? states.hover : states.base);
b.style.boxShadow = '';
};
Object.entries({
mouseover: () => b.style.background = states.hover.background,
mouseout: () => !pressed && (b.style.background = states.base.backgroundColor),
mousedown: () => {
pressed = true;
Object.assign(b.style, states.active);
document.addEventListener('mouseup', handleUp, { once: true });
}
}).forEach(([e, f]) => b.addEventListener(e, f));
return b;
};
const buttonsContainer = r.parentNode;
const fillBtn = createButton('SKU填充', handlers.fillHandler);
const replaceBtn = createButton('SKU替换', handlers.replaceHandler);
buttonsContainer.append(fillBtn, replaceBtn);
console.log('SKU功能已加载');
};
const init = () => {
const c = document.evaluate('//div[@class="front"]', document, null, 9).singleNodeValue;
const r = c?.querySelector('button:not([data-sku])');
c && r && createSkuButtons(c, r);
};
const start = () => {
init();
const timer = setInterval(() => {
const existingButtons = document.querySelectorAll('[data-sku]');
if (existingButtons.length < 2) init();
}, 1e3);
const c = document.evaluate('//div[@class="front"]', document, null, 9).singleNodeValue;
c && (c._observer = new MutationObserver(() => {
const existingButtons = c.querySelectorAll('[data-sku]');
if (existingButtons.length < 2) init();
})).observe(c, { childList: true, subtree: true });
window.addEventListener('unload', () => {
clearInterval(timer);
c?._observer?.disconnect();
});
};
(function checkContainer() {
document.evaluate('//div[@class="front"]', document, null, 9).singleNodeValue
? start()
: setTimeout(checkContainer, 100);
})();
};
injectSkuButton();
})();