feat(home): add task operation dialog and improve UI interactions - Add TaskOperationDialog component to home page and connect via event emitter - Enhance ChatBox "智能问数" button with hover styles and cursor pointer - Improve ChatInputArea button hover states and cursor styling - Fix selector logic in fg_trace.js to handle date column bounds checking - Reorder package.json dependencies for consistency
179 lines
6.0 KiB
JavaScript
179 lines
6.0 KiB
JavaScript
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}`;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
const clickWithRetry = async (locator, { attempts = 6 } = {}) => {
|
|
let lastError;
|
|
for (let i = 0; i < attempts; i++) {
|
|
try {
|
|
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
await locator.waitFor({ state: 'visible', timeout: 8000 });
|
|
await locator.click({ timeout: 8000, force: true });
|
|
return;
|
|
} catch (e) {
|
|
lastError = e;
|
|
try {
|
|
await locator.page().keyboard.press('Escape');
|
|
} catch (e2) {}
|
|
await locator.page().waitForTimeout(400 + Math.random() * 600);
|
|
}
|
|
}
|
|
throw lastError;
|
|
};
|
|
|
|
const ensureRoomPriceCalendar = async (page) => {
|
|
const container = page.locator('#price-reserve-table-container');
|
|
try {
|
|
if (await container.isVisible({ timeout: 1500 })) return;
|
|
} catch (e) {}
|
|
|
|
const menuTitle = page.getByRole('menuitem', { name: /房价房量管理/ }).first();
|
|
await clickWithRetry(menuTitle);
|
|
|
|
const calendarEntry = page.getByRole('menuitem', { name: /房价房量日历/ }).first();
|
|
await clickWithRetry(calendarEntry);
|
|
|
|
await container.waitFor({ state: 'visible', timeout: 30000 });
|
|
};
|
|
|
|
const findHeaderDateIndex = async (container, mmdd) => {
|
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
const headerCells = container.locator(
|
|
"xpath=.//div[contains(@class,'headerRow')]/div[contains(@class,'headerContent')]",
|
|
);
|
|
const count = await headerCells.count();
|
|
for (let i = 0; i < count; i++) {
|
|
const text = (await headerCells.nth(i).innerText()).replace(/\s+/g, ' ').trim();
|
|
if (text.includes(mmdd)) return i;
|
|
}
|
|
|
|
await container.evaluate((el) => {
|
|
el.scrollLeft += 600;
|
|
});
|
|
await container.page().waitForTimeout(120);
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
const toggleRoomByDateIndex = async (container, { roomType, dateIndex, operation }) => {
|
|
const strictAnchor = container.getByText(roomType, { exact: true }).first();
|
|
const looseAnchor = container.getByText(roomType).first();
|
|
const roomAnchor = (await strictAnchor.count()) > 0 ? strictAnchor : looseAnchor;
|
|
if ((await roomAnchor.count()) === 0) {
|
|
throw new Error(`Room type not found in table: ${roomType}`);
|
|
}
|
|
await roomAnchor.scrollIntoViewIfNeeded();
|
|
|
|
const boardRow = roomAnchor.locator("xpath=ancestor::div[contains(@class,'boardRow')][1]");
|
|
const dayColumns = boardRow.locator(
|
|
"xpath=.//div[contains(@class,'switchbar')]/ancestor::div[contains(@class,'flex-col')][1]",
|
|
);
|
|
const dayColumnCount = await dayColumns.count();
|
|
if (dateIndex >= dayColumnCount) {
|
|
throw new Error(
|
|
`Date column out of range for room ${roomType}: index=${dateIndex}, available=${dayColumnCount}`,
|
|
);
|
|
}
|
|
const dayColumn = dayColumns.nth(dateIndex);
|
|
|
|
const targetStateClass = operation === 'open' ? 'error' : 'success';
|
|
const expectedCurrentLabel = operation === 'open' ? '满' : '有';
|
|
const barByLabel = dayColumn
|
|
.locator(
|
|
`xpath=.//*[ (contains(@class,'success') or contains(@class,'error')) and .//*[contains(normalize-space(),'${expectedCurrentLabel}')] ]//*[contains(@class,'bar')]`,
|
|
)
|
|
.first();
|
|
const barByClass = dayColumn
|
|
.locator(`xpath=.//*[contains(@class,'${targetStateClass}')]//*[contains(@class,'bar')]`)
|
|
.first();
|
|
const bar = (await barByLabel.count()) > 0 ? barByLabel : barByClass;
|
|
|
|
await bar.waitFor({ state: 'visible' });
|
|
await bar.scrollIntoViewIfNeeded();
|
|
await bar.click();
|
|
};
|
|
|
|
(async () => {
|
|
let browser;
|
|
|
|
try {
|
|
const targetUrl = 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1';
|
|
const prepared = await preparePage(chromium, { targetUrl });
|
|
browser = prepared.browser;
|
|
const page = prepared.page;
|
|
|
|
await page.waitForTimeout(4000 + Math.random() * 300);
|
|
|
|
await ensureRoomPriceCalendar(page);
|
|
|
|
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;
|
|
}
|
|
|
|
const container = page.locator('#price-reserve-table-container');
|
|
await container.waitFor({ state: 'visible' });
|
|
|
|
const dateList = buildDateList(startDate, endDate);
|
|
for (const mmdd of dateList) {
|
|
const dateIndex = await findHeaderDateIndex(container, mmdd);
|
|
if (dateIndex < 0) {
|
|
throw new Error(`Date not found in header: ${mmdd}`);
|
|
}
|
|
await toggleRoomByDateIndex(container, { roomType, dateIndex, operation });
|
|
await page.waitForTimeout(600 + Math.random() * 600);
|
|
}
|
|
} catch (error) {
|
|
log.error(error);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await safeDisconnectBrowser(browser);
|
|
process.exit(process.exitCode || 0);
|
|
}
|
|
})();
|