import { chromium } from 'playwright'; import log from 'electron-log'; import tabsPkg from './common/tabs.js'; const { preparePage, safeDisconnectBrowser } = tabsPkg; const parseDateInput = (dateStr) => { if (!dateStr) return null; if (typeof dateStr === 'number' && Number.isFinite(dateStr)) return new Date(dateStr); if (dateStr instanceof Date && !Number.isNaN(dateStr.getTime())) return dateStr; const raw = String(dateStr).trim(); const ymdMatch = raw.match(/(\d{4}-\d{2}-\d{2})/); if (ymdMatch) return new Date(`${ymdMatch[1]}T00:00:00`); const mmddMatch = raw.match(/(\d{2}-\d{2})/); if (mmddMatch) { const year = new Date().getFullYear(); return new Date(`${year}-${mmddMatch[1]}T00:00:00`); } return null; }; const formatMMDD = (d) => { const mm = String(d.getMonth() + 1).padStart(2, '0'); const dd = String(d.getDate()).padStart(2, '0'); return `${mm}.${dd}`; // Meituan uses MM.DD format }; const buildDateList = (startDate, endDate) => { const start = parseDateInput(startDate); const end = parseDateInput(endDate || startDate); if (!start || !end) return []; const from = start <= end ? start : end; const to = start <= end ? end : start; const dates = []; const cur = new Date(from.getTime()); while (cur <= to) { dates.push(formatMMDD(cur)); cur.setDate(cur.getDate() + 1); } return dates; }; // Find the column class (e.g. "col_55") for a specific date const findDateColumnClass = async (page, mmdd) => { // Try visible headers first const headerCells = page.locator('.vxe-header--column'); const checkHeaders = async () => { const c = await headerCells.count(); for (let i = 0; i < c; i++) { const cell = headerCells.nth(i); const text = (await cell.innerText()).replace(/\s+/g, ' ').trim(); if (text.includes(mmdd)) { // Extract the col_X class const classList = await cell.getAttribute('class'); const match = classList.match(/col_\d+/); if (match) return match[0]; } } return null; }; let colClass = await checkHeaders(); if (colClass) return colClass; // Try scrolling if not found const scrollContainer = page.locator('.vxe-table--body-wrapper').first(); if (await scrollContainer.isVisible()) { for (let attempt = 0; attempt < 10; attempt++) { await scrollContainer.evaluate((el) => { el.scrollLeft += 600; }); await page.waitForTimeout(300); colClass = await checkHeaders(); if (colClass) return colClass; } } return null; }; // Find the row index for a specific room type const findRoomRowIndex = async (page, roomType) => { // We look for the row in ANY part of the table (fixed left or main) // and assume row indexes align across split tables. const rows = page.locator('.vxe-body--row'); const count = await rows.count(); for (let i = 0; i < count; i++) { const text = await rows.nth(i).innerText(); if (text.includes(roomType)) { // Found the row. Now we need to determine its "logical" index. // If the table is split into left/main, 'rows' contains ALL rows from both tables mixed or sequential. // A safer way is to find the specific TR and get its rowIndex or check its parent. // Optimization: Usually room names are in the fixed-left wrapper. // Let's try to find the row specifically in the left wrapper first if it exists. const leftRows = page.locator('.vxe-table--fixed-left-wrapper .vxe-body--row'); if (await leftRows.count() > 0) { const leftCount = await leftRows.count(); for(let j=0; j { // 1. Find the column class for the date (e.g. "col_6") const colClass = await findDateColumnClass(page, mmdd); if (!colClass) { throw new Error(`Date column not found for ${mmdd}`); } // log.info(`Date ${mmdd} maps to column class: ${colClass}`); // 2. Find the row index for the room const rowIndex = await findRoomRowIndex(page, roomType); if (rowIndex === -1) { throw new Error(`Room row not found for ${roomType}`); } // log.info(`Room ${roomType} maps to row index: ${rowIndex}`); // 3. Locate the target cell in the MAIN body wrapper (where switches usually are) // using the row index and column class. const mainBody = page.locator('.vxe-table--body-wrapper'); const targetRow = mainBody.locator('.vxe-body--row').nth(rowIndex); const targetCell = targetRow.locator(`.${colClass}`); // 4. Find and toggle switch const switchEl = targetCell.locator('.lz-switch').first(); if (await switchEl.count() === 0) { log.warn(`No switch found at ${roomType} / ${mmdd} (Row ${rowIndex}, Col ${colClass})`); return; } await switchEl.scrollIntoViewIfNeeded(); // Check state // Inspecting common switch classes: 'lz-switch-active' (Meituan), 'is-checked', etc. const classList = await switchEl.getAttribute('class'); const isChecked = classList && ( classList.includes('is-checked') || classList.includes('checked') || classList.includes('lz-switch-checked') || classList.includes('lz-switch-active') ); const isOpen = isChecked; if (operation === 'open' && !isOpen) { await switchEl.click(); log.info(`Opened room ${roomType} on ${mmdd}`); } else if (operation === 'close' && isOpen) { await switchEl.click(); log.info(`Closed room ${roomType} on ${mmdd}`); } else { log.info(`Room ${roomType} already ${operation === 'open' ? 'open' : 'closed'} on ${mmdd}`); } }; (async () => { let browser; try { const targetUrl = 'https://me.meituan.com/ebooking/merchant/product#/index'; const prepared = await preparePage(chromium, { targetUrl }); browser = prepared.browser; const page = prepared.page; await page.waitForTimeout(4000 + Math.random() * 300); // Navigation logic from user snippet try { const menu = page.locator('#vina-menu-item-100114001 > .lz-submenu-title'); if (await menu.isVisible()) { await menu.click(); await page.waitForTimeout(1000); } else { // Fallback if the submenu title isn't there const menuFallback = page.locator('#vina-menu-item-100114001'); if (await menuFallback.isVisible()) { await menuFallback.click(); await page.waitForTimeout(1000); } } const calendarLink = page.locator('a').filter({ hasText: '房价房量日历' }); if (await calendarLink.isVisible()) { await calendarLink.click(); await page.waitForTimeout(3000); } } catch (e) { log.warn('Navigation attempt failed, assuming already on page:', e.message); } const roomType = process.env.ROOM_TYPE; const startDate = process.env.START_DATE; const endDate = process.env.END_DATE; const operation = process.env.OPERATION === 'close' ? 'close' : 'open'; if (!roomType || !startDate) { log.info('ROOM_TYPE/START_DATE not provided, skip room toggle.'); return; } // Wait for table await page.locator('.vxe-table--body-wrapper').waitFor({ state: 'visible', timeout: 15000 }); const dateList = buildDateList(startDate, endDate); for (const mmdd of dateList) { try { await toggleRoom(page, { roomType, mmdd, operation }); } catch (e) { log.error(`Failed to toggle ${roomType} on ${mmdd}: ${e.message}`); } await page.waitForTimeout(600 + Math.random() * 600); } } catch (error) { log.error(error); process.exitCode = 1; } finally { await safeDisconnectBrowser(browser); process.exit(process.exitCode || 0); } })();