一键成品面板:输入“型号 商品名称”后自动拆分填写;只保留一个按钮。
// ==UserScript==
// @name XSJv3.1
// @namespace http://tampermonkey.net/
// @version 28.0.1112.3
// @description 一键成品面板:输入“型号 商品名称”后自动拆分填写;只保留一个按钮。
// @author Assistant
// @match *://agentseller.temu.com/goods*
// @grant none
// @license y
// ==/UserScript==
(function () {
'use strict';
/* ─── 配置区 ────────────────────────────────────────────────────── */
const CONFIG = {
queue: [
['商品产地', '中国大陆'],
['关闭', '不关闭'],
['包含的组件', '无'],
['衬里说明', '无衬里'],
['油边', '否'],
['货源产地', '广州产区'],
['护理说明', '不可洗'],
['特征', '固定肩带'],
['图案样式', '其他印花'],
['印花类型', '定位印花'],
['风格', '休闲'],
['材料', '涤纶'],
],
province: '广东省',
parentSpec1: { typeValueId: 1001, specValues: ['黄色'] },
parentSpec2: { typeValueId: 45114199, specValues: ['套装A', '套装B', '套装C', '套装D', '套装E'] },
dimensions: {
default: ['34', '30', '6', '335'],
byModel: {
'套装A': ['32', '20', '3', '260'],
'套装B': ['32', '27', '6', '430'],
'套装C': ['32', '20', '3', '327'],
'套装D': ['32', '27', '6.5', '470'],
'套装E': ['32', '27', '6.5', '490']
}
},
sizeChart: [
{ label: '宽度', min: '30', max: '33' },
{ label: '长度', min: '40', max: '43' }
],
sku: {
skuTypeId: 1,
skuClassLabel: '混合套装',
declarePriceByModel: {
'套装A': '30',
'套装B': '40',
'套装C': '38',
'套装D': '43',
'套装E': '45'
},
singleItemCountByModel: {
'套装A': '2',
'套装B': '2',
'套装C': '3',
'套装D': '3',
'套装': '3'
},
packMode: '不是独立包装',
currency: 'CNY',
suggestPrice: '199',
packageItemCount: '1',
packageListByModel: {
'套装A': ['托特包', '化妆包'],
'套装B': ['托特包', '帽子'],
'套装C': ['托特包', '化妆包', '帽子']
},
cargoNo: 'NYZX'
}
};
const STATE = {
modelText: '',
productNameText: ''
};
/* ─── 时间常量 ──────────────────────────────────────────────────── */
const T = {
MICRO: 80,
SHORT: 150,
MED: 300,
SPEC: 1800
};
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const clean = (t) => (t || '').replace(/\s+/g, ' ').trim();
function parseInputLine(raw) {
const text = String(raw || '').replace(/\u00A0/g, ' ').trim();
if (!text) {
return null;
}
const m = text.match(/^(\S+)\s+(.+)$/);
if (!m) {
return null;
}
return {
model: clean(m[1]),
name: clean(m[2])
};
}
function readPanelData() {
const input = document.getElementById('temu-onekey-source');
const parsed = parseInputLine(input?.value || '');
if (!parsed) {
throw new Error('请输入“型号 商品名称”,中间至少一个空格或 Tab');
}
STATE.modelText = parsed.model;
STATE.productNameText = parsed.name;
CONFIG.sku.cargoNo = parsed.model;
return parsed;
}
/* ════════════════════════════════════════════════════════════════
原点击/输入工具
════════════════════════════════════════════════════════════════ */
async function superClick(el) {
if (!el) return;
el.scrollIntoView({ block: 'center' });
await wait(0);
const rect = el.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
[
new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse', clientX, clientY }),
new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, clientX, clientY }),
new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse', clientX, clientY }),
new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, clientX, clientY }),
new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX, clientY })
].forEach((ev) => el.dispatchEvent(ev));
}
function fastClick(el) {
if (!el) return;
const opts = { bubbles: true, view: window };
el.dispatchEvent(new MouseEvent('mousedown', opts));
el.dispatchEvent(new MouseEvent('click', opts));
el.dispatchEvent(new MouseEvent('mouseup', opts));
}
function setReactValue(el, val) {
if (!el) return;
const prev = el.value;
el.value = val;
const t = el._valueTracker;
if (t) t.setValue(prev);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function setVal(el, val) {
if (!el) return;
const prev = el.value;
el.value = val;
const tracker = el._valueTracker;
if (tracker) tracker.setValue(prev);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function click(el) {
if (!el) return;
el.scrollIntoView({ block: 'center' });
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const s = { bubbles: true, cancelable: true, view: window, clientX: cx, clientY: cy };
['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) =>
el.dispatchEvent(type.startsWith('pointer')
? new PointerEvent(type, { ...s, pointerType: 'mouse' })
: new MouseEvent(type, s))
);
}
async function pickOptionExact(val) {
await wait(0);
const popups = Array.from(document.querySelectorAll(
'.beast-select-dropdown:not([style*="display: none"]), .ST_popupWrapper_5-120-1, [class*="ST_popup"]'
)).filter((el) => window.getComputedStyle(el).display !== 'none');
const lastPopup = popups[popups.length - 1] || document;
const opts = Array.from(lastPopup.querySelectorAll(
'.ST_item_5-120-1, .beast-select-item-option-content, [role="option"], li'
));
const hit = opts.find((o) => o.innerText.trim() === val)
|| opts.find((o) => o.innerText.trim().includes(val));
if (hit) {
fastClick(hit);
return true;
}
return false;
}
function findItemV26(labelText) {
for (const el of document.querySelectorAll('.Form_itemLabelContent_5-120-1')) {
const txt = el.innerText.trim();
if (txt === labelText || txt.startsWith(labelText)) {
return el.closest('.Form_item_5-120-1');
}
}
return null;
}
async function selectDropdown(labelText, val) {
if (Array.isArray(val)) {
for (const item of val) {
await selectDropdown(labelText, item);
await wait(T.SHORT);
}
return;
}
const item = findItemV26(labelText);
if (!item) return;
const trigger = item.querySelector('.ST_outerWrapper_5-120-1, .beast-select-selector, input');
document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27, bubbles: true }));
await wait(0);
fastClick(trigger);
const input = item.querySelector('input');
if (input) {
setReactValue(input, val);
await wait(0);
}
await pickOptionExact(val);
}
async function selectProvince() {
const provInp = document.querySelector('input[placeholder="请选择省份"]');
if (!provInp) return;
fastClick(provInp.closest('.ST_outerWrapper_5-120-1') || provInp);
setReactValue(provInp, CONFIG.province);
await pickOptionExact(CONFIG.province);
}
async function clickAddParentSpec2() {
const target = Array.from(document.querySelectorAll('div, span, button'))
.filter((el) => (el.innerText || '').includes('添加父规格 2') && el.offsetWidth > 0)
.reverse()[0];
if (target) {
await superClick(target);
} else {
const fallback = Array.from(document.querySelectorAll('button, div, span'))
.find((el) => el.innerText.trim() === '添加父规格' && el.offsetWidth > 0);
if (fallback) await superClick(fallback);
}
}
function getCtrl(el) {
if (!el) return null;
const key = Object.keys(el).find(
(k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
);
let node = key ? el[key] : null;
let d = 0;
while (node && d < 20) {
const p = node.memoizedProps || {};
if (Array.isArray(p.options) && typeof p.onChange === 'function') return p;
node = node.return;
d++;
}
return null;
}
function pickById(ctrl, valueId) {
if (!ctrl) return false;
const opt = ctrl.options.find((o) => String(o.value) === String(valueId));
if (!opt) return false;
ctrl.onChange(opt.value, opt);
return true;
}
function pickByLabel(ctrl, label) {
if (!ctrl) return false;
const opt = ctrl.options.find((o) => clean(o.label) === label)
|| ctrl.options.find((o) => clean(o.label).includes(label));
if (!opt) return false;
ctrl.onChange(opt.value, opt);
return true;
}
function findFormItem(labelText) {
const target = clean(labelText).replace(/\s+/g, '');
for (const el of document.querySelectorAll('[class*="Form_itemLabelContent"]')) {
const t = clean(el.innerText);
const nt = t.replace(/\s+/g, '');
if (t === labelText || t.startsWith(labelText) || nt === target || nt.startsWith(target)) {
return el.closest('[class*="Form_item_"]');
}
}
return null;
}
function findSectionRootByText(sectionLabel) {
const target = clean(sectionLabel).replace(/\s+/g, '');
const sections = [...document.querySelectorAll('div, section, form, article')];
const byText = sections.find((el) => {
const text = clean(el.innerText || '');
const nt = text.replace(/\s+/g, '');
return (text.includes(sectionLabel) || nt.includes(target)) && text.includes('批量填写');
}) || null;
if (byText) return byText;
const byInput = [...document.querySelectorAll('input, textarea')].find((el) => {
const hint = clean(el.placeholder || '');
const owner = clean(el.closest('div, section, form, article')?.innerText || '');
const nh = hint.replace(/\s+/g, '');
const no = owner.replace(/\s+/g, '');
return (hint.includes(sectionLabel) || nh.includes(target) || owner.includes(sectionLabel) || no.includes(target));
});
return byInput ? byInput.closest('div, section, form, article') : null;
}
function findVolumeWeightSectionRoot() {
return [...document.querySelectorAll('[data-testid="beast-core-form-item"], div, section, form, article')]
.find((el) => {
const text = clean(el.innerText || '').replace(/\s+/g, '');
return text.includes('敏感属性与体积重量') && text.includes('体积(单位:cm)') && text.includes('重量');
}) || null;
}
function findVolumeWeightBatchButton(root) {
const scope = root || findVolumeWeightSectionRoot();
if (!scope) return null;
return scope.querySelector('.batch-input_batchContainer__GINEg button')
|| [...scope.querySelectorAll('button, span, div')]
.find((el) => clean(el.innerText) === '批量填写')
|| null;
}
function findInput(labelText, { last = true } = {}) {
const item = findFormItem(labelText);
if (!item) return null;
const inputs = [...item.querySelectorAll(
'input:not([readonly]):not([type="radio"]):not([type="checkbox"]):not([type="file"]), textarea'
)];
return inputs.length ? (last ? inputs[inputs.length - 1] : inputs[0]) : null;
}
function findParentSpecTypeInputs() {
return [...document.querySelectorAll('input')]
.filter((el) => {
if (!el.readOnly) return false;
const ctrl = getCtrl(el);
return ctrl?.options?.some((o) => ['1001', '45114199'].includes(String(o.value)));
});
}
function findSpecBlockRoot(specTypeEl) {
let root = specTypeEl.closest(
'[class*="Form_item_"], [class*="specRow"], [class*="SpecRow"], tr, [class*="row_"], [class*="Row_"]'
) || specTypeEl.parentElement;
let node = root;
let depth = 0;
while (node && depth < 8) {
const text = clean(node.innerText || '');
if (text.includes('继续添加子规格') || text.includes('添加子规格')) {
root = node;
}
node = node.parentElement;
depth++;
}
return root || specTypeEl.parentElement;
}
function listEditableSpecInputs(root) {
return [...(root || document).querySelectorAll('input, textarea')]
.filter((el) => !el.readOnly && !el.disabled)
.filter((el) => !['file', 'radio', 'checkbox', 'hidden'].includes((el.type || '').toLowerCase()));
}
function findSpecificSpecTableRoot(labelText) {
const normalize = (t) => clean(t).replace(/^\+\s*/, '');
const tables = [...document.querySelectorAll('table.TB_tableWrapper_5-120-1')];
return tables.find((table) => {
const text = clean(table.innerText || '');
const hasContinue = [...table.querySelectorAll('button, div, span, a')]
.some((el) => normalize(el.innerText) === '继续添加子规格');
return hasContinue && text.includes(`*${labelText}`);
}) || null;
}
async function fillOneParentSpec(specTypeEl, typeValueId, specTextValue) {
const ctrl = getCtrl(specTypeEl);
if (!pickById(ctrl, typeValueId)) {
pickByLabel(ctrl, String(typeValueId));
}
await wait(T.MED);
const blockRoot = findSpecBlockRoot(specTypeEl);
const container = specTypeEl.closest(
'[class*="specRow"], [class*="SpecRow"], tr, [class*="row_"], [class*="Row_"]'
) || blockRoot || specTypeEl.parentElement?.parentElement;
let valueEl = container
? container.querySelector('input:not([readonly]):not([disabled]):not([type="file"]):not([type="radio"]):not([type="checkbox"])')
: null;
if (!valueEl) {
const all = [...document.querySelectorAll('input')];
const selfIdx = all.indexOf(specTypeEl);
valueEl = all.slice(selfIdx + 1, selfIdx + 6)
.find((el) => !el.readOnly && !el.disabled && !['file', 'radio', 'checkbox'].includes(el.type));
}
if (valueEl) {
valueEl.focus();
setVal(valueEl, specTextValue);
await wait(T.SHORT);
} else {
console.warn('[Temu助手] 找不到规格值输入框');
}
return { blockRoot, valueEl };
}
async function clickAddSpec2Fallback() {
const target = Array.from(document.querySelectorAll('div, span, button'))
.filter((el) => (el.innerText || '').includes('添加父规格 2') && el.offsetWidth > 0)
.reverse()[0];
if (target) {
await superClick(target);
return;
}
const fallback = Array.from(document.querySelectorAll('button, div, span'))
.find((el) => el.innerText.trim() === '添加父规格' && el.offsetWidth > 0);
if (fallback) {
await superClick(fallback);
return;
}
throw new Error('未找到“添加父规格 2”按钮');
}
async function clickContinueAddChildSpec(root = document) {
const normalize = (t) => clean(t).replace(/^\+\s*/, '');
const btn = [...root.querySelectorAll('button')]
.find((el) => normalize(el.innerText) === '继续添加子规格')
|| [...root.querySelectorAll('div, span, a')]
.find((el) => normalize(el.innerText) === '继续添加子规格');
if (!btn) throw new Error('未找到“继续添加子规格”按钮');
click(btn.closest('button') || btn);
await wait(T.MED);
}
async function fillParentSpecs() {
const appendChildSpecValues = async (values, root) => {
let inputs = listEditableSpecInputs(root);
for (let i = 1; i < values.length; i++) {
const beforeCount = inputs.length;
await clickContinueAddChildSpec(root);
const start = Date.now();
while (true) {
inputs = listEditableSpecInputs(root);
if (inputs.length > beforeCount) break;
if (Date.now() - start > 5000) throw new Error('新增子规格输入框超时');
await wait(120);
}
const input = inputs[beforeCount];
if (!input) throw new Error(`未找到子规格输入框 ${i + 1}`);
input.focus();
setVal(input, values[i]);
await wait(T.SHORT);
}
};
let specInputs = findParentSpecTypeInputs();
if (!specInputs.length) {
console.warn('[Temu助手] 未找到父规格1');
return;
}
const parentSpec1Result = await fillOneParentSpec(specInputs[0], CONFIG.parentSpec1.typeValueId, CONFIG.parentSpec1.specValues[0]);
const parentSpec1Root = findSpecificSpecTableRoot('颜色') || parentSpec1Result.blockRoot;
await appendChildSpecValues(CONFIG.parentSpec1.specValues, parentSpec1Root);
await clickAddParentSpec2();
specInputs = findParentSpecTypeInputs();
if (specInputs.length < 2) {
console.warn('[Temu助手] 未找到父规格2');
return;
}
const parentSpec2Result = await fillOneParentSpec(specInputs[1], CONFIG.parentSpec2.typeValueId, CONFIG.parentSpec2.specValues[0]);
const parentSpec2Root = findSpecificSpecTableRoot('型号') || parentSpec2Result.blockRoot;
await appendChildSpecValues(CONFIG.parentSpec2.specValues, parentSpec2Root);
}
function isEditableTextControl(el) {
if (!el) return false;
if (el.disabled || el.readOnly) return false;
if (el.matches('textarea')) return true;
if (el.matches('input')) {
const t = (el.type || '').toLowerCase();
return !['file', 'radio', 'checkbox', 'hidden'].includes(t);
}
return false;
}
function findProductNameInput() {
const exactLabelEls = [...document.querySelectorAll('div, span, label, p')]
.filter((el) => clean(el.innerText) === '商品名称' && el.offsetParent !== null);
for (const labelEl of exactLabelEls) {
let cur = labelEl;
for (let i = 0; i < 6 && cur; i++) {
const scope = cur.parentElement || cur;
const controls = [...scope.querySelectorAll('textarea, input')]
.filter(isEditableTextControl)
.filter((el) => {
const text = clean(el.closest('[class*="Form_item_"], div, section, form')?.innerText || '');
return !text.includes('英文名称');
});
if (controls.length) return controls[0];
cur = cur.parentElement;
}
let next = labelEl.nextElementSibling;
let guard = 0;
while (next && guard < 8) {
const controls = [...next.querySelectorAll('textarea, input')]
.filter(isEditableTextControl)
.filter((el) => {
const text = clean(el.closest('[class*="Form_item_"], div, section, form')?.innerText || '');
return !text.includes('英文名称');
});
if (controls.length) return controls[0];
next = next.nextElementSibling;
guard++;
}
}
const candidates = [...document.querySelectorAll('textarea[placeholder="请输入"], input[placeholder="请输入"]')]
.filter(isEditableTextControl)
.filter((el) => {
const areaText = clean(el.closest('[class*="Form_item_"], form, section, div')?.innerText || '');
return areaText.includes('商品名称') && !areaText.includes('英文名称');
});
if (candidates.length) return candidates[0];
return null;
}
async function fillProductName() {
const input = findProductNameInput();
if (!input) {
console.warn('[Temu助手] 未找到商品名称输入框');
return;
}
input.focus();
setVal(input, STATE.productNameText);
await wait(T.SHORT);
}
async function fillDimensions() {
const volumeRoot = findVolumeWeightSectionRoot();
const volumeTable = volumeRoot?.querySelector('table.performance-table_performanceTable__dwfgW');
if (volumeTable) {
const rows = [...volumeTable.querySelectorAll('tbody tr')];
for (const row of rows) {
const rowText = clean(row.innerText || '');
const modelName = Object.keys(CONFIG.dimensions.byModel).find((name) => rowText.includes(name));
const rowValues = CONFIG.dimensions.byModel[modelName] || CONFIG.dimensions.default;
const inputs = [...row.querySelectorAll('input[placeholder="请输入"]')]
.filter((el) => !el.disabled && !el.readOnly);
if (inputs.length < rowValues.length) {
console.warn('[Temu助手] 体积重量行输入框数量不足');
continue;
}
for (let i = 0; i < rowValues.length; i++) {
inputs[i].focus();
setVal(inputs[i], rowValues[i]);
await wait(60);
}
}
return;
}
}
function modalBody() {
return document.querySelector('[data-testid="beast-core-modal-body"]');
}
async function openSizeChartDialog() {
const trigger = [...document.querySelectorAll('button, span, div')]
.find((el) => {
const txt = clean(el.innerText);
return txt === '添加尺码表' || txt === '编辑尺码表';
});
if (!trigger) throw new Error('未找到“添加尺码表”按钮');
click(trigger.closest('button') || trigger);
await wait(500);
const start = Date.now();
while (!modalBody()) {
if (Date.now() - start > 5000) throw new Error('尺码表弹层未打开');
await wait(100);
}
}
function getBaseCheckBoxes() {
const modal = modalBody();
if (!modal) return [];
const labels = new Set(CONFIG.sizeChart.map((item) => `${item.label}(cm)`));
return [...modal.querySelectorAll('label[data-testid="beast-core-checkbox"]')]
.filter((label) => labels.has(clean(label.innerText)));
}
async function waitUntilBaseMetricsReady() {
const expected = CONFIG.sizeChart.length;
const start = Date.now();
while (true) {
const boxes = getBaseCheckBoxes();
if (boxes.length === expected) return boxes;
if (Date.now() - start > 5000) throw new Error('尺码表基础选项未就绪');
await wait(100);
}
}
async function clickBaseMetrics() {
const boxes = await waitUntilBaseMetricsReady();
for (const label of boxes) {
const checkbox = label.querySelector('input[type="checkbox"]');
if (checkbox && !checkbox.checked) {
click(checkbox);
await wait(120);
}
}
}
async function ensureRangeCheckboxes() {
const modal = modalBody();
if (!modal) throw new Error('未找到尺码表弹层');
const expectedLabels = CONFIG.sizeChart.map((item) => `${item.label} 范围区间`);
const start = Date.now();
while (true) {
const text = clean(modal.innerText);
if (expectedLabels.every((label) => text.includes(label))) {
const rangeBoxes = [...modal.querySelectorAll('input[type="checkbox"]')].slice(-expectedLabels.length);
if (rangeBoxes.length === expectedLabels.length) return rangeBoxes;
}
if (Date.now() - start > 5000) throw new Error('范围区间复选框未就绪');
await wait(100);
}
}
function getRangeInputs() {
const modal = modalBody();
if (!modal) return [];
return [...modal.querySelectorAll('input[type="text"], textarea')]
.filter((el) => !el.disabled && !el.readOnly)
.filter((el) => clean(el.placeholder) === '请输入');
}
async function fillSizeChart() {
await openSizeChartDialog();
await clickBaseMetrics();
const rangeBoxes = await ensureRangeCheckboxes();
for (const box of rangeBoxes) {
if (!box.checked) {
click(box);
await wait(150);
}
}
const start = Date.now();
const expectedInputCount = CONFIG.sizeChart.length * 2;
while (getRangeInputs().length < expectedInputCount) {
if (Date.now() - start > 5000) throw new Error('范围输入框未出现');
await wait(100);
}
const inputs = getRangeInputs();
const values = CONFIG.sizeChart.flatMap((item) => [item.min, item.max]);
for (let i = 0; i < values.length; i++) {
setVal(inputs[i], values[i]);
await wait(100);
}
const confirm = [...document.querySelectorAll('button, span, div')]
.find((el) => clean(el.innerText) === '确认');
if (!confirm) throw new Error('未找到尺码表确认按钮');
click(confirm.closest('button') || confirm);
const closeStart = Date.now();
while (modalBody()) {
if (Date.now() - closeStart > 5000) break;
await wait(100);
}
await wait(300);
}
async function selectSensitiveNo() {
const input = [...document.querySelectorAll('input')].find((el) => clean(el.placeholder) === '敏感属性');
const trigger = input
? input.closest('[class*="ST_outerWrapper"], [class*="IPT_inputWrapper"], [class*="IPT_outerWrapper"], div')
: [...document.querySelectorAll('div, span, input')].find((el) => {
const txt = clean(el.innerText || el.value || el.placeholder || '');
return txt.includes('敏感属性') && !txt.includes('说明');
});
if (!trigger) throw new Error('未找到敏感属性触发器');
click(trigger);
await wait(300);
const popup = [...document.querySelectorAll('div, section, form, article')]
.filter((el) => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
const text = clean(el.innerText || '');
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0
&& text.includes('是否敏感品')
&& text.includes('敏感属性');
})
.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
return (br.width * br.height) - (ar.width * ar.height);
})[0] || document;
const magneticLabel = [...popup.querySelectorAll('label')]
.find((el) => clean(el.innerText) === '否')
|| [...popup.querySelectorAll('span, div')]
.find((el) => clean(el.innerText) === '否');
if (!magneticLabel) throw new Error('未找到“否”选项');
const checkbox = magneticLabel.closest('label')?.querySelector('input[type="checkbox"]')
|| magneticLabel.parentElement?.querySelector?.('input[type="checkbox"]')
|| magneticLabel.previousElementSibling?.querySelector?.('input[type="checkbox"]')
|| null;
if (checkbox) {
if (!checkbox.checked) click(checkbox);
} else {
click(magneticLabel.closest('label') || magneticLabel);
}
await wait(300);
}
async function clickBatchFillInSection(sectionLabel) {
if (sectionLabel === '敏感属性与体积重量') {
const root = findVolumeWeightSectionRoot();
if (!root) throw new Error('未找到“敏感属性与体积重量”区域');
const target = findVolumeWeightBatchButton(root);
if (!target) throw new Error('未找到体积重量批量填写按钮');
const btn = target.closest('button') || target;
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(160);
return;
}
const item = findSectionRootByText(sectionLabel) || findFormItem(sectionLabel);
if (!item) throw new Error(`未找到“${sectionLabel}”区域`);
const btn = [...item.querySelectorAll('button, span, div')]
.find((el) => clean(el.innerText) === '批量填写');
if (!btn) throw new Error(`未找到“${sectionLabel}”区域里的“批量填写”按钮`);
const target = btn.closest('button') || btn;
if (typeof target.click === 'function') target.click();
else click(target);
await wait(160);
}
function findSkuBatchSectionRoot() {
return document.querySelector('.product-sku_skuTableContainer__sX1e0')
|| [...document.querySelectorAll('div, section, form, article')]
.find((el) => {
const text = clean(el.innerText || '').replace(/\s+/g, '');
return text.includes('SKU信息')
&& text.includes('批量填写')
&& text.includes('申报价格(CNY)')
&& text.includes('SKU分类')
&& text.includes('建议零售价');
}) || null;
}
function findSkuBatchTable() {
return document.querySelector('.product-sku_skuTableContainer__sX1e0 table.performance-table_performanceTable__dwfgW')
|| findSkuBatchSectionRoot()?.querySelector('table.performance-table_performanceTable__dwfgW')
|| null;
}
function findSelectInputIn(root) {
return root?.querySelector('input[data-testid="beast-core-select-htmlInput"]') || null;
}
async function pickSelectValueIn(root, label) {
const input = findSelectInputIn(root);
const ctrl = getCtrl(input);
if (ctrl && pickByLabel(ctrl, label)) {
await wait(T.SHORT);
return true;
}
const trigger = root?.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
if (!trigger) return false;
click(trigger);
await wait(T.SHORT);
return await pickOptionExact(label);
}
async function pickPackModeValueIn(root, label) {
const trigger = root?.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
if (!trigger) return false;
click(trigger);
await wait(T.SHORT);
const popup = [...document.querySelectorAll('[role="listbox"], .ST_dropdownPanel_5-120-1, .beast-select-dropdown, .ST_popupWrapper_5-120-1, [class*="ST_popup"]')]
.filter((el) => {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
})
.reverse()
.find((el) => clean(el.innerText || '').includes('是独立包装') || clean(el.innerText || '').includes('不是独立包装'));
if (!popup) return false;
const option = [...popup.querySelectorAll('[role="option"], li, .beast-select-item-option-content, .ST_item_5-120-1')]
.find((el) => clean(el.innerText) === label || clean(el.innerText).includes(label));
if (!option) return false;
click(option);
await wait(T.SHORT);
return true;
}
function findPackModeRootInRow(row) {
const direct = row.querySelector('[id*=".productSkuMultiPack.packIncludeInfo"]');
if (direct) return direct;
const categoryItems = [...row.querySelectorAll('.sku-category_formItem__iqG7r [data-testid="beast-core-form-item"]')];
if (categoryItems.length >= 3) return categoryItems[2];
return [...row.querySelectorAll('[data-testid="beast-core-form-item"]')].find((el) => {
const text = clean(el.innerText || '');
return text.includes('请选择独立包装') || text.includes('不是独立包装') || text.includes('独立包装');
}) || null;
}
function findModelNameInRow(row) {
const rowText = clean(row?.innerText || '');
return CONFIG.parentSpec2.specValues.find((name) => rowText.includes(name)) || '';
}
function listPackageRootsInRow(row) {
const supplierRoot = row.querySelector('[id$=".supplierPrice"]');
const baseId = (supplierRoot?.id || '').replace(/\.supplierPrice$/, '');
const selector = baseId
? `[id^="${baseId}.packageInventoryList["]`
: '[id*=".packageInventoryList["]';
return [...document.querySelectorAll(selector)]
.sort((a, b) => {
const ai = Number((a.id.match(/\[(\d+)\]/) || [])[1] || 0);
const bi = Number((b.id.match(/\[(\d+)\]/) || [])[1] || 0);
return ai - bi;
});
}
async function clickPackageAddInRow(row) {
const roots = listPackageRootsInRow(row);
const anchorRoot = roots[roots.length - 1] || row;
const packageCell = anchorRoot.closest('td') || anchorRoot.parentElement || row;
const btn = [...packageCell.querySelectorAll('button, a, span, div')]
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
return text === '+ 添加' || text.includes('+ 添加') || text.includes('添加');
})
|| [...packageCell.querySelectorAll('a[class*="BTN_outerWrapperLink"], button[class*="BTN_textPrimary"], a[class*="BTN_textPrimary"]')].at(-1)
|| null;
if (!btn) return false;
const target = btn.closest('button, a') || btn;
if (typeof target.click === 'function') target.click();
else click(target);
await wait(260);
return true;
}
async function clickConfirmIfVisible() {
const confirm = [...document.querySelectorAll('button, [role="button"]')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
if (text !== '确认') return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
})
.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
return (br.top - ar.top) || (br.left - ar.left);
})[0];
if (!confirm) return false;
const target = confirm.closest('button') || confirm;
if (typeof target.click === 'function') target.click();
click(target);
await wait(260);
return true;
}
async function ensurePackageRootsCount(row, expectedCount) {
let roots = listPackageRootsInRow(row);
const addTimes = Math.max(0, expectedCount - 1);
for (let i = 0; i < addTimes; i++) {
const beforeCount = roots.length;
const ok = await clickPackageAddInRow(row);
if (!ok) break;
const start = Date.now();
while (Date.now() - start < 3000) {
roots = listPackageRootsInRow(row);
if (roots.length > beforeCount) break;
await wait(100);
}
if (roots.length <= beforeCount) break;
}
return listPackageRootsInRow(row);
}
async function fillPackageInventoryInRow(row, modelName) {
const items = CONFIG.sku.packageListByModel[modelName] || [];
if (!items.length) return;
const roots = await ensurePackageRootsCount(row, items.length);
for (let i = 0; i < items.length; i++) {
const root = roots[i];
if (!root) continue;
const selectWrapper = root.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
const selectInput = root.querySelector('input[data-testid="beast-core-select-htmlInput"]');
let ok = false;
if (selectWrapper) {
click(selectWrapper);
await wait(T.SHORT);
}
if (selectInput) {
setReactValue(selectInput, items[i]);
await wait(T.SHORT);
ok = await pickOptionExact(items[i]);
}
if (!ok) {
ok = await pickSelectValueIn(root, items[i]);
}
if (!ok) {
console.warn(`[Temu助手] 未能设置包装清单项=${items[i]}`);
}
await clickConfirmIfVisible();
await wait(45);
const countInput = root.querySelector('input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="数量"], input[placeholder="请输入"]');
if (countInput && !countInput.disabled && !countInput.readOnly) {
setVal(countInput, CONFIG.sku.packageItemCount);
await wait(45);
}
}
}
async function fillSkuBatchTable() {
const table = findSkuBatchTable();
if (!table) return false;
const rows = [...table.querySelectorAll('tbody tr')].filter((row) => row.querySelector('[id$=".supplierPrice"]'));
if (!rows.length) return false;
for (const row of rows) {
const modelName = findModelNameInRow(row);
const declarePrice = CONFIG.sku.declarePriceByModel[modelName] || CONFIG.sku.declarePriceByModel['套装A'];
const singleItemCount = CONFIG.sku.singleItemCountByModel[modelName] || CONFIG.sku.singleItemCountByModel['套装A'];
const declareRoot = row.querySelector('[id$=".supplierPrice"]');
const declareInput = declareRoot?.querySelector('input[placeholder="请输入"]');
if (declareInput && !declareInput.disabled && !declareInput.readOnly) {
setVal(declareInput, declarePrice);
await wait(45);
}
const skuClassRoot = row.querySelector('[id*=".productSkuMultiPack.skuClassification"]');
if (skuClassRoot) {
const ok = await pickSelectValueIn(skuClassRoot, CONFIG.sku.skuClassLabel);
if (!ok) console.warn(`[Temu助手] 未能设置 SKU分类=${CONFIG.sku.skuClassLabel}`);
await wait(100);
}
const countRoot = row.querySelector('[id*=".productSkuMultiPack.numberOfInfo"]');
const countInput = countRoot?.querySelector('input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="请输入"]');
if (countInput && !countInput.disabled && !countInput.readOnly) {
setVal(countInput, singleItemCount);
await wait(45);
}
const packModeRoot = findPackModeRootInRow(row);
if (packModeRoot) {
const ok = await pickPackModeValueIn(packModeRoot, CONFIG.sku.packMode)
|| await pickSelectValueIn(packModeRoot, CONFIG.sku.packMode);
if (!ok) console.warn(`[Temu助手] 未能设置 包装模式=${CONFIG.sku.packMode}`);
await wait(70);
}
await fillPackageInventoryInRow(row, modelName);
const suggestRoot = row.querySelector('[id$=".suggestSalesPrice"]');
const suggestInput = suggestRoot?.querySelector('input[data-testid="beast-core-input-htmlInput"], input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="请输入"]');
if (suggestInput && !suggestInput.disabled && !suggestInput.readOnly) {
setVal(suggestInput, CONFIG.sku.suggestPrice);
await wait(70);
}
const selectWrappers = suggestRoot
? [...suggestRoot.querySelectorAll('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]')]
: [];
const currencyRoot = selectWrappers[selectWrappers.length - 1] || null;
if (currencyRoot) {
const ok = await pickSelectValueIn(currencyRoot, CONFIG.sku.currency);
if (!ok) console.warn(`[Temu助手] 未能设置建议零售价币种=${CONFIG.sku.currency}`);
await wait(45);
}
}
return true;
}
async function fillSkuBlock() {
const skuBatchFilled = await fillSkuBatchTable();
if (skuBatchFilled) {
const cargoEl = findInput('货号');
if (cargoEl) {
setVal(cargoEl, CONFIG.sku.cargoNo);
await wait(100);
} else {
console.warn('[Temu助手] 未找到货号输入');
}
return;
}
if (findSkuBatchSectionRoot()) {
throw new Error('SKU 信息批量填写表已打开,但未匹配到可填写行');
}
const allInputs = [...document.querySelectorAll('input, textarea')];
const headerEl = allInputs.find((el) => el.placeholder === '申报价格' && el.readOnly);
let rowInputs = [];
if (headerEl) {
const headerRow = headerEl.closest('tr, [class*="tableRow"], [class*="TableRow"]');
const table = headerEl.closest('table, [class*="Table"]');
if (headerRow && table) {
const dataRow = [...table.querySelectorAll('tr, [class*="tableRow"], [class*="TableRow"]')]
.find((r) => r !== headerRow && !headerRow.contains(r));
if (dataRow) {
rowInputs = [...dataRow.querySelectorAll(
'input:not([type="file"]):not([type="radio"]):not([type="checkbox"]), textarea'
)];
}
}
}
if (!rowInputs.length && headerEl) {
const idx = allInputs.indexOf(headerEl);
rowInputs = allInputs.slice(idx + 1, idx + 20)
.filter((el) => !['file', 'radio', 'checkbox'].includes(el.type));
}
if (!rowInputs.length) {
console.warn('[Temu助手] 未找到 SKU 数据行输入框');
}
const declareEl = rowInputs.find(
(el) => !el.readOnly && !el.disabled
&& ['text', ''].includes(el.type)
&& !getCtrl(el)?.options?.length
);
if (declareEl) {
setVal(declareEl, CONFIG.sku.declarePrice);
await wait(100);
} else {
console.warn('[Temu助手] 未找到申报价格输入');
}
const skuTypeEl = rowInputs.find((el) => {
const ctrl = getCtrl(el);
return ctrl?.options?.some((o) => o.value === 1 || o.value === '1');
});
if (skuTypeEl) {
pickByLabel(getCtrl(skuTypeEl), CONFIG.sku.skuClassLabel) || pickById(getCtrl(skuTypeEl), CONFIG.sku.skuTypeId);
await wait(T.SHORT);
} else {
console.warn('[Temu助手] 未找到 SKU 分类选择器');
}
const currencyEl = rowInputs.find((el) => {
const ctrl = getCtrl(el);
return ctrl?.options?.length >= 50;
});
if (currencyEl) {
const ctrl = getCtrl(currencyEl);
const cnOpt = ctrl.options.find((o) => String(o.value) === CONFIG.sku.currency);
if (cnOpt) {
ctrl.onChange(cnOpt.value, cnOpt);
await wait(T.SHORT);
}
} else {
console.warn('[Temu助手] 未找到货币选择器');
}
const priceInputs = rowInputs.filter(
(el) => !el.readOnly && !el.disabled
&& ['text', ''].includes(el.type)
&& !getCtrl(el)?.options?.length
);
const suggestEl = priceInputs[1] || null;
if (suggestEl) {
setVal(suggestEl, CONFIG.sku.suggestPrice);
await wait(100);
} else {
console.warn('[Temu助手] 未找到建议零售价输入');
}
const cargoEl = findInput('货号');
if (cargoEl) {
setVal(cargoEl, CONFIG.sku.cargoNo);
await wait(100);
} else {
console.warn('[Temu助手] 未找到货号输入');
}
}
const STEPS = [
{
name: '属性下拉',
fn: async () => {
for (const [name, val] of CONFIG.queue) await selectDropdown(name, val);
}
},
{ name: '货源省份', fn: () => selectProvince() },
{ name: '父规格', fn: () => fillParentSpecs() },
{ name: '商品名称', fn: () => fillProductName() },
{ name: '等待规格生成', fn: () => wait(T.SPEC) },
{ name: '尺码表', fn: () => fillSizeChart() },
{ name: '敏感批量', fn: async () => { await clickBatchFillInSection('敏感属性与体积重量'); } },
{ name: '尺寸重量', fn: () => fillDimensions() },
{ name: '敏感属性', fn: async () => { await selectSensitiveNo(); await wait(200); } },
{ name: 'SKU 批量', fn: async () => { await clickBatchFillInSection('SKU 信息'); } },
{ name: 'SKU 填写', fn: () => fillSkuBlock() }
];
async function run(btn) {
if (btn.dataset.running === 'true') return;
readPanelData();
btn.dataset.running = 'true';
const label = btn.querySelector('.temu-label');
const orig = label.textContent;
try {
if (typeof window.__TEMU_UPLOAD_PREPARE__ === 'function') {
label.textContent = '预授权图片目录';
await window.__TEMU_UPLOAD_PREPARE__();
}
for (const [i, step] of STEPS.entries()) {
label.textContent = `${i + 1}/${STEPS.length} ${step.name}`;
await step.fn();
}
if (typeof window.__TEMU_SKU_RUNFILL__ === 'function') {
label.textContent = `附加 1/2 SKU脚本`;
await window.__TEMU_SKU_RUNFILL__();
}
if (typeof window.__TEMU_UPLOAD_RUN__ === 'function') {
label.textContent = `附加 2/2 图片脚本`;
await window.__TEMU_UPLOAD_RUN__(STATE.modelText);
}
btn.style.setProperty('--bg', '#4caf50');
label.textContent = '完成 ✓';
} catch (e) {
console.error('[Temu助手]', e);
btn.style.setProperty('--bg', '#f44336');
label.textContent = '出错 ✗';
alert(e.message || '执行失败');
}
setTimeout(() => {
btn.dataset.running = 'false';
btn.style.setProperty('--bg', 'rgba(255,87,34,.9)');
label.textContent = orig;
}, 2200);
}
function injectUI() {
if (document.getElementById('temu-v28-panel')) return;
const style = document.createElement('style');
style.textContent = `
#temu-v28-panel {
position: fixed;
left: 15px;
top: 15px;
z-index: 100000;
width: 286px;
background: rgba(255,255,255,.96);
border: 1px solid rgba(0,0,0,.12);
border-radius: 12px;
box-shadow: 0 8px 22px rgba(0,0,0,.15);
padding: 8px 10px 10px;
font: 12px/1.3 sans-serif;
color: #222;
user-select: none;
}
#temu-v28-dragbar {
width: 44px;
height: 4px;
margin: 0 auto 8px;
border-radius: 999px;
background: rgba(0,0,0,.18);
cursor: move;
}
#temu-onekey-source {
width: 100%;
box-sizing: border-box;
height: 30px;
border: 1px solid #d9d9d9;
border-radius: 8px;
padding: 5px 10px;
outline: none;
font: 12px/1.1 monospace;
color: #222;
background: #fff;
}
#temu-onekey-source:focus {
border-color: #ff5722;
box-shadow: 0 0 0 2px rgba(255,87,34,.12);
}
#temu-v28-btn {
margin-top: 8px;
width: 100%;
padding: 8px 12px;
border: 0;
border-radius: 8px;
background: rgba(255,87,34,.95);
color: #fff;
font: bold 13px/1 sans-serif;
cursor: pointer;
user-select: none;
transition: opacity .2s, background .3s;
}
#temu-v28-btn:hover { opacity: .88; }
#temu-v28-btn[data-running="true"] { opacity: .92; cursor: wait; }
`;
document.head.appendChild(style);
const panel = document.createElement('div');
panel.id = 'temu-v28-panel';
panel.innerHTML = `
<div id="temu-v28-dragbar"></div>
<input id="temu-onekey-source" type="text" placeholder="型号 商品名称" value="${STATE.modelText} ${STATE.productNameText}" />
<button id="temu-v28-btn" type="button" data-running="false"><span class="temu-label">一键成品</span></button>
`;
document.body.appendChild(panel);
const dragbar = panel.querySelector('#temu-v28-dragbar');
let dragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
dragbar.addEventListener('pointerdown', (e) => {
dragging = true;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
const rect = panel.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
dragbar.setPointerCapture(e.pointerId);
e.preventDefault();
});
dragbar.addEventListener('pointermove', (e) => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = `${Math.max(0, startLeft + dx)}px`;
panel.style.top = `${Math.max(0, startTop + dy)}px`;
});
const stopDrag = () => { dragging = false; };
dragbar.addEventListener('pointerup', stopDrag);
dragbar.addEventListener('pointercancel', stopDrag);
window.addEventListener('pointerup', stopDrag);
const btn = panel.querySelector('#temu-v28-btn');
btn.addEventListener('click', () => run(btn));
}
injectUI();
new MutationObserver(injectUI).observe(document.body, { childList: true });
/* ════════════════════════════════════════════════════════════════
第二脚本逻辑保留,仅不单独注入按钮
════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
const CONFIG = {
declaredPrice: '27',
retailPrice: '159',
retailCurrency: 'CNY',
skuClass: '单品',
maxEdge: '31',
midEdge: '18',
minEdge: '1',
weight: '120',
};
const wait = ms => new Promise(r => setTimeout(r, ms));
function fastClick(el) {
if (!el) return;
['mousedown', 'click', 'mouseup'].forEach(n =>
el.dispatchEvent(new MouseEvent(n, { bubbles: true, view: window }))
);
}
function setReactValue(el, val) {
if (!el) return;
const lastValue = el.value;
el.value = val;
const event = new Event('input', { bubbles: true });
const tracker = el._valueTracker;
if (tracker) tracker.setValue(lastValue);
el.dispatchEvent(event);
el.dispatchEvent(new Event('change', { bubbles: true }));
}
const byPh = ph => {
const inputs = Array.from(document.querySelectorAll(`input[placeholder*="${ph}"]`));
return inputs.find(inp => {
const wrapper = inp.closest('.beast-input-inner-wrapper');
const hasCurrencySymbol = wrapper?.innerText.includes('¥') ||
wrapper?.parentElement?.innerText.includes('¥');
return hasCurrencySymbol && !inp.closest('[class*="batch"], [class*="Batch"]');
}) || inputs.find(inp => !inp.closest('[class*="batch"]'));
};
function findInputNear(labelText) {
const labels = Array.from(document.querySelectorAll('*')).filter(el =>
el.childElementCount === 0 && el.innerText?.trim() === labelText && !el.closest('[class*="batch"]')
);
for (const el of labels) {
let p = el.parentElement;
for (let i = 0; i < 5; i++) {
if (!p) break;
const inp = p.querySelector('input:not([readonly])');
if (inp) return inp;
p = p.parentElement;
}
}
return null;
}
async function selectOption(trigger, optionText) {
if (!trigger) return false;
fastClick(trigger);
await wait(250);
const opts = Array.from(document.querySelectorAll('.beast-select-item-option-content, [role="option"], li'));
const hit = opts.find(o => o.innerText?.trim() === optionText) || opts.find(o => o.innerText?.includes(optionText));
if (hit) {
fastClick(hit);
await wait(100);
return true;
}
return false;
}
async function runFill() {
try {
const declaredInp = byPh('申报价格');
if (declaredInp) setReactValue(declaredInp, CONFIG.declaredPrice);
const retailInp = byPh('建议零售价') || byPh('零售价');
if (retailInp) setReactValue(retailInp, CONFIG.retailPrice);
const currTrigger = Array.from(document.querySelectorAll('[class*="ST_outerWrapper"], [class*="appendCell"]'))
.find(el => !el.closest('[class*="batch"], [class*="Batch"]') && ['USD', 'CNY', 'JPY'].includes(el.innerText?.trim()));
if (currTrigger) await selectOption(currTrigger, CONFIG.retailCurrency);
const skuInp = byPh('SKU分类');
if (skuInp) await selectOption(skuInp.closest('[class*="ST_outerWrapper"]') || skuInp, CONFIG.skuClass);
const dims = { '最长边': CONFIG.maxEdge, '次长边': CONFIG.midEdge, '最短边': CONFIG.minEdge };
for (const [k, v] of Object.entries(dims)) {
const inp = byPh(k) || findInputNear(k);
if (inp) setReactValue(inp, v);
}
const wtInp = findInputNear('g')?.parentElement?.querySelector('input') || findInputNear('重量');
if (wtInp) setReactValue(wtInp, CONFIG.weight);
} catch (e) {
console.error('[Temu SKU信息自动填写]', e);
throw e;
}
}
window.__TEMU_SKU_RUNFILL__ = runFill;
})();
})();
// ==UserScript==
// @name Temu 商品素材图上传 3.1x
// @namespace http://tampermonkey.net/
// @version 0.4.1
// @description 上传商品素材图和外包装图片:输入货号,自动把该货号文件夹内全部图片上传到“商品素材图 -> 素材中心 -> 本地上传”,并把最后一张图上传到外包装图片。
// @author Assistant
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp']);
const ROOT_KEY = 'temu_material_root_dir_v1';
const ROOT_HINT = 'C:\\Users\\1\\Desktop\\新建文件夹 (2)\\NYZX包\\11111';
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const clean = (t) => (t || '').replace(/\s+/g, ' ').trim();
function isVisible(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
}
function click(el) {
if (!el) return;
el.scrollIntoView({ block: 'center', inline: 'center' });
const rect = el.getBoundingClientRect();
const shared = {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2,
};
['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
el.dispatchEvent(type.startsWith('pointer')
? new PointerEvent(type, { ...shared, pointerType: 'mouse' })
: new MouseEvent(type, shared));
});
}
function setFiles(input, files) {
const dt = new DataTransfer();
for (const file of files) dt.items.add(file);
input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
function compareFileNames(a, b) {
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
}
async function pickImagesFromDir(dirHandle) {
const files = [];
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind !== 'file') continue;
const dot = name.lastIndexOf('.');
const ext = dot >= 0 ? name.slice(dot).toLowerCase() : '';
if (!IMAGE_EXTS.has(ext)) continue;
files.push(await handle.getFile());
}
files.sort(compareFileNames);
return files;
}
async function openDb() {
return await new Promise((resolve, reject) => {
const req = indexedDB.open('temu-material-root-db', 1);
req.onupgradeneeded = () => {
req.result.createObjectStore('kv');
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function saveRootHandle(handle) {
try {
const db = await openDb();
await new Promise((resolve, reject) => {
const tx = db.transaction('kv', 'readwrite');
tx.objectStore('kv').put(handle, ROOT_KEY);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
db.close();
} catch (e) {
console.warn('[Temu素材图] 保存根目录句柄失败', e);
}
}
async function loadRootHandle() {
try {
const db = await openDb();
const handle = await new Promise((resolve, reject) => {
const tx = db.transaction('kv', 'readonly');
const req = tx.objectStore('kv').get(ROOT_KEY);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => reject(req.error);
});
db.close();
if (!handle) return null;
if (handle.queryPermission && (await handle.queryPermission({ mode: 'read' })) !== 'granted') {
return null;
}
return handle;
} catch {
return null;
}
}
async function ensureRootHandle() {
const stored = await loadRootHandle();
if (stored) return stored;
const handle = await window.showDirectoryPicker({ mode: 'read', startIn: 'documents' });
await saveRootHandle(handle);
return handle;
}
async function prepareRootHandle() {
await ensureRootHandle();
}
async function getSkuImages(sku) {
if (!sku) throw new Error('请输入货号');
const rootHandle = await ensureRootHandle();
const skuDir = await rootHandle.getDirectoryHandle(sku, { create: false });
const images = await pickImagesFromDir(skuDir);
if (!images.length) throw new Error(`货号文件夹里没有图片:${sku}`);
return images;
}
function findFormItem(labelText) {
return [...document.querySelectorAll('[class*="Form_itemLabelContent"]')]
.find((el) => {
const t = clean(el.innerText);
return t === labelText || t.startsWith(labelText);
})?.closest('[class*="Form_item_"]') || null;
}
function findMaterialItem() {
const exact = findFormItem('商品素材图');
if (exact) return exact;
return [...document.querySelectorAll('div, section, form, article')]
.find((el) => {
const t = clean(el.innerText);
return t.includes('商品素材图') && (t.includes('素材中心') || t.includes('上传列表') || t.includes('本地上传'));
}) || null;
}
function findCarouselItem() {
const exact = findFormItem('商品轮播图');
if (exact) return exact;
return [...document.querySelectorAll('div, section, form, article')]
.find((el) => clean(el.innerText).includes('商品轮播图')) || null;
}
function findOuterPackagingItem() {
const exact = findFormItem('商品包装信息');
if (exact) return exact;
return [...document.querySelectorAll('div, section, form, article')]
.find((el) => {
const t = clean(el.innerText);
return t.includes('外包装图片') && t.includes('批量上传');
}) || null;
}
function findPreviewItemsByModel() {
const table = document.querySelector('.product-sku_skuTableContainer__sX1e0 table.performance-table_performanceTable__dwfgW')
|| document.querySelector('table.performance-table_performanceTable__dwfgW');
if (!table) return [];
const rows = [...table.querySelectorAll('tbody tr')];
return rows.map((row) => {
const modelCell = [...row.querySelectorAll('td')]
.map((td) => clean(td.innerText))
.find((text) => /^套装[A-Z]$/.test(text));
const previewItem = row.querySelector('[id$=".previewImgsI18n.common"]');
return modelCell && previewItem ? { model: modelCell, item: previewItem } : null;
}).filter(Boolean);
}
function findUploadTrigger(root) {
if (!root) return null;
return root.querySelector('.upload-trigger_wrapProduct__caAk7, .upload-trigger_wrap__kMsdx')
|| [...root.querySelectorAll('button, div, span, label, a')].find((el) => {
if (!isVisible(el)) return false;
const t = clean(el.innerText);
return t.includes('素材中心');
})
|| null;
}
function findClickableText(root, texts) {
if (!root) return null;
const terms = Array.isArray(texts) ? texts : [texts];
return [...root.querySelectorAll('button, div, span, label, a')]
.find((el) => {
if (!isVisible(el)) return false;
const t = clean(el.innerText);
return terms.some((term) => t === term || t.includes(term));
}) || null;
}
function findTopDialogWithText(text) {
const dialogs = [...document.querySelectorAll('div, section, form, article')].filter((el) => {
const t = clean(el.innerText);
return t.includes(text) && isVisible(el);
});
if (!dialogs.length) return null;
return dialogs.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
return (br.width * br.height) - (ar.width * ar.height);
})[0];
}
async function waitFor(fn, timeoutMs = 10000, stepMs = 120) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const result = fn();
if (result) return result;
await wait(stepMs);
}
return null;
}
async function ensureMaterialCenterDialog() {
const dialog = findTopDialogWithText('素材中心') || findTopDialogWithText('上传列表') || findTopDialogWithText('本地上传');
if (!dialog) throw new Error('未找到素材中心弹层');
return dialog;
}
async function openMaterialCenterFromGoodsMaterial() {
const item = findCarouselItem() || findMaterialItem();
if (!item) throw new Error('未找到商品轮播图区域');
const trigger = findUploadTrigger(item) || findClickableText(item, ['素材中心', '进入素材中心']);
if (!trigger) throw new Error('未找到商品轮播图入口');
click(trigger.closest('button') || trigger);
let dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 4000);
if (!dialog) {
click(item);
await wait(120);
click(trigger.closest('button') || trigger);
dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 8000);
}
if (!dialog) throw new Error('商品素材图素材中心没有打开');
return dialog;
}
async function handleUploadResultDialog() {
const listBtn = await waitFor(() => {
const candidates = [...document.querySelectorAll('button, div, span, label, a')];
return candidates.find((el) => isVisible(el) && clean(el.innerText) === '在列表中查看')
|| candidates.find((el) => clean(el.innerText) === '在列表中查看')
|| null;
}, 8000);
if (listBtn) {
click(listBtn.closest('button') || listBtn);
await wait(600);
}
}
async function waitForPreviewListViewReady(timeoutMs = 8000) {
return await waitFor(() => {
const dialog = findTopDialogWithText('素材中心') || findTopDialogWithText('上传列表');
if (!dialog) return null;
const candidates = [...dialog.querySelectorAll('button, div, span, label, a')];
return candidates.find((el) => isVisible(el) && clean(el.innerText) === '在列表中查看')
|| candidates.find((el) => clean(el.innerText) === '在列表中查看')
|| null;
}, timeoutMs, 120);
}
async function clickConfirmInMaterialCenter() {
const waitStart = Date.now();
let confirm = null;
while (!confirm && Date.now() - waitStart < 6000) {
confirm = [...document.querySelectorAll('button, [role="button"]')]
.filter((el) => isVisible(el))
.map((el) => ({ el, r: el.getBoundingClientRect(), text: clean(el.innerText || el.textContent || '') }))
.filter(({ text, r }) => text === '确认' && r.width > 60 && r.height > 24)
.sort((a, b) => b.r.top - a.r.top)
.at(0)?.el
|| [...document.querySelectorAll('button, [role="button"]')]
.find((el) => {
if (!isVisible(el)) return false;
const t = clean(el.innerText || el.textContent || '');
return t === '确认';
}) || null;
if (!confirm) await wait(120);
}
if (confirm) {
const target = confirm.closest('button') || confirm;
if (typeof target.click === 'function') target.click();
else click(target);
await wait(400);
await waitFor(() => !findTopDialogWithText('素材中心') && !findTopDialogWithText('上传列表'), 5000, 120);
return true;
}
return false;
}
function baseName(file) {
return file.name.replace(/\.[^.]+$/, '');
}
function findMaterialCardsInDialog(dialog) {
return [...dialog.querySelectorAll('div[class*="cardContainer"]')]
.filter(isVisible)
.map((card) => {
const text = clean(card.textContent);
const nameEl = [...card.querySelectorAll('div,span,a')].find((el) => {
const t = clean(el.textContent);
return t && !t.includes('裁剪') && !t.includes('美化') && !t.includes('翻译') && /^[A-Za-z0-9_-]+$/.test(t);
});
return {
card,
text,
name: nameEl ? clean(nameEl.textContent) : ''
};
})
.filter((item) => item.name);
}
async function reorderMaterialCards(files) {
const dialog = findTopDialogWithText('上传列表') || await ensureMaterialCenterDialog();
const cards = findMaterialCardsInDialog(dialog);
if (!cards.length) return;
const targetNames = files.map(baseName);
const matchedCards = targetNames
.map((name) => cards.find((card) => card.name === name || card.text.includes(name)))
.filter(Boolean);
if (!matchedCards.length) return;
for (const item of matchedCards) {
if (item.card.className.includes('checked')) {
click(item.card);
await wait(120);
}
}
for (const item of matchedCards) {
click(item.card);
await wait(150);
}
}
async function waitForMaterialUploadInput(timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const input = [...document.querySelectorAll('input[type="file"]')].find((el) => {
const accept = (el.getAttribute('accept') || '').toLowerCase();
const parentText = clean(el.parentElement?.textContent || el.closest('div,section,article,form')?.textContent || '');
return parentText.includes('本地上传') && accept.includes('.png') && accept.includes('.jpg');
});
if (input) return input;
await wait(100);
}
return null;
}
async function waitForOuterPackagingUploadInput(timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const input = [...document.querySelectorAll('input[type="file"]')].find((el) => {
const accept = (el.getAttribute('accept') || '').toLowerCase();
const parentText = clean(el.parentElement?.textContent || el.closest('div,section,article,form')?.textContent || '');
return parentText.includes('批量上传') && accept.includes('.png') && accept.includes('.jpg');
});
if (input) return input;
await wait(100);
}
return null;
}
async function uploadCarouselImages(images) {
await openMaterialCenterFromGoodsMaterial();
const fileInput = await waitForMaterialUploadInput(10000);
if (!fileInput) throw new Error('未找到素材中心里的本地上传输入框');
setFiles(fileInput, images);
await wait(1400);
await handleUploadResultDialog();
await wait(850);
await reorderMaterialCards(images);
await wait(650);
await clickConfirmInMaterialCenter();
}
async function uploadOuterPackagingLastImage(images) {
const lastImage = images[images.length - 1];
const item = findOuterPackagingItem();
if (!item) throw new Error('未找到外包装图片区');
const trigger = findClickableText(item, ['批量上传']);
if (!trigger) throw new Error('未找到外包装图片上传按钮');
click(trigger.closest('button') || trigger);
const fileInput = await waitForOuterPackagingUploadInput(10000);
if (!fileInput) throw new Error('未找到外包装图片的本地上传输入框');
setFiles(fileInput, [lastImage]);
await wait(200);
}
async function uploadPreviewLastImage(images) {
const previewItems = findPreviewItemsByModel();
if (!previewItems.length) throw new Error('未找到预览图区域');
const imageByModel = {
'套装A': images[images.length - 1] || null,
'套装B': images[images.length - 2] || images[images.length - 1] || null,
'套装C': images[images.length - 3] || images[images.length - 2] || images[images.length - 1] || null
};
for (const { model, item } of previewItems) {
const image = imageByModel[model];
if (!image) continue;
const trigger = findUploadTrigger(item) || findClickableText(item, ['素材中心', '预览图']);
if (!trigger) {
console.warn(`[Temu助手] 未找到 ${model} 的预览图素材中心入口`);
continue;
}
click(trigger.closest('button') || trigger);
const dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 8000);
if (!dialog) throw new Error(`${model} 预览图素材中心没有打开`);
await wait(550);
const fileInput = await waitForMaterialUploadInput(10000);
if (!fileInput) throw new Error(`未找到 ${model} 预览图素材中心里的本地上传输入框`);
setFiles(fileInput, [image]);
await wait(300);
await waitForPreviewListViewReady(8000);
await handleUploadResultDialog();
await wait(300);
await clickConfirmInMaterialCenter();
await wait(180);
}
}
async function uploadMaterialAndOuterPackaging(sku) {
const images = await getSkuImages(sku);
await uploadCarouselImages(images);
await uploadOuterPackagingLastImage(images);
await wait(250);
await uploadPreviewLastImage(images);
}
function readSharedSku() {
const source = document.getElementById('temu-onekey-source');
const raw = clean(source?.value || '');
if (!raw) return '';
return clean(raw.split(/\s+/)[0] || '');
}
window.__TEMU_UPLOAD_RUN__ = async function (sku) {
const finalSku = clean(sku || readSharedSku());
if (!finalSku) throw new Error('未找到可用于上传图片的货号');
await uploadMaterialAndOuterPackaging(finalSku);
};
window.__TEMU_UPLOAD_PREPARE__ = prepareRootHandle;
})();