Repository navigation
Expand file tree
/
Copy pathuploader.js
More file actions
113 lines (96 loc) · 3.56 KB
/
Copy pathuploader.js
File metadata and controls
113 lines (96 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// uploader.js — Puppeteer automation: open form, fill fields, upload image, save
import { sleep, applyTemplate, log } from './utils.js';
// Form selectors on the target admin page — change these to match your site
export const SELECTORS = {
addButton: 'button[onclick="showAddForm()"]',
price: '#productGia',
data: '#dulieu',
image: '#productAvatar',
code: '#noibat',
saveButton: 'button[onclick="saveProduct()"]',
};
async function waitFor(page, selector, timeout = 6000) {
try {
await page.waitForSelector(selector, { timeout });
return await page.$(selector);
} catch {
throw new Error(`Timeout: không tìm thấy "${selector}" sau ${timeout}ms`);
}
}
// Set a value and fire input/change so the page's JS sees the update
async function setField(page, selector, value) {
if (!value && value !== 0) return false;
const el = await page.$(selector);
if (!el) return false;
await page.$eval(selector, (el, val) => {
el.value = String(val);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, String(value));
return true;
}
async function openAddForm(page) {
const btn = await page.$(SELECTORS.addButton);
if (btn) {
await btn.click();
await sleep(300);
}
}
async function fillOneProduct(page, product, note, dataTemplate) {
const resolvedNote = product.note || note || '';
await waitFor(page, SELECTORS.price, 5000);
await setField(page, SELECTORS.price, product.gia);
if (!(await page.$(SELECTORS.data))) throw new Error(`Không tìm thấy ${SELECTORS.data}`);
await setField(page, SELECTORS.data, applyTemplate(dataTemplate, product.maSP, product.gia, resolvedNote));
if (product.filePath) {
const avatarInput = await waitFor(page, SELECTORS.image, 5000);
await avatarInput.uploadFile(product.filePath);
await sleep(300);
}
await setField(page, SELECTORS.code, product.maSP);
}
async function clickSaveButton(page) {
const btn = await page.$(SELECTORS.saveButton);
if (!btn) throw new Error(`Không tìm thấy nút LƯU (${SELECTORS.saveButton})`);
await btn.click();
await sleep(300);
}
// Reload the admin page with a cache-busting param so each product starts clean
async function navigateClean(page, adminUrl) {
try {
const u = new URL(adminUrl);
u.searchParams.set('_au', Date.now().toString());
await page.goto(u.toString(), { waitUntil: 'domcontentloaded', timeout: 15000 });
} catch {
await page.goto(adminUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
}
await sleep(600);
}
// Upload one product, retrying up to 3 times on failure
export async function uploadOne(page, product, config, attempt = 1) {
const MAX_RETRY = 3;
const { note, dataTemplate, adminUrl, delay, dryRun } = config;
try {
await navigateClean(page, adminUrl);
await openAddForm(page);
await fillOneProduct(page, product, note, dataTemplate);
if (delay > 0) await sleep(delay);
if (dryRun) {
log.dryrun(`[DRY RUN] Sẽ lưu: ${product.maSP}`);
} else {
await clickSaveButton(page);
await Promise.race([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 }).catch(() => {}),
sleep(5000),
]);
}
return { status: 'success' };
} catch (err) {
if (attempt < MAX_RETRY) {
log.warn(`Thử lại lần ${attempt}/${MAX_RETRY}: ${err.message}`);
await sleep(1000);
return uploadOne(page, product, config, attempt + 1);
}
throw new Error(`Lỗi sau ${MAX_RETRY} lần thử: ${err.message}`);
}
}